برنامه نویسی

نحوه ایجاد یک جهان خودمختار نامتقارن اطلاعاتی: MUD + Circom

Summarize this content to 400 words in Persian Lang
در این آموزش، ما یک بازی 100% روی زنجیره با عدم تقارن اطلاعاتی ایجاد خواهیم کرد، به عبارت دیگر، یک دنیای مستقل که حالت خصوصی و محاسبات آن در اتریوم کاملاً قابل تأیید است. ما از MUD، موتور جهان های خودمختار، و Circom، پرکاربردترین زبان برای مدارهای ZK استفاده خواهیم کرد.

شما یاد خواهید گرفت که:

زبان Circom را با چارچوب MUD ترکیب کنید
یک دنیای مستقل با محاسبات و متغیرهای خصوصی ایجاد کنید
اثبات های zk-SNARK را مستقیماً از مرورگر خود با استفاده از Snark.js ایجاد کنید

فهرست مطالب

بازی

در شروع بازی، هر بازیکن 4 واحد تولید می کند که می تواند در نقشه حرکت کند. هر واحد از نوع متفاوتی است، اما این اطلاعات خصوصی است و فقط برای صاحب واحدها قابل مشاهده است.

🐉 که 🧙 را می زند که 🧌 را می زند که 🗡️ را می زند. اما 🗡️ یک واحد ویژه است زیرا تنها واحدی است که می تواند 🐉 را شکست دهد

چهار نوع تعریف شده در بازی عبارتند از 🐉، که 🧙 را شکست می دهد، که 🧌 را شکست می دهد، که 🗡️ را شکست می دهد. با این حال، 🗡️ یک واحد ویژه است زیرا تنها واحدی است که می تواند 🐉 را شکست دهد. نبردها با استفاده از یک zk-SNARK انجام می شود که نوع مهاجم را نشان می دهد و به دنبال آن zk-SNARK دیگری که نتیجه نبرد را بدون مشخص کردن نوع واحد مورد حمله محاسبه می کند.

معماری بازی تمام داده های عمومی و منطق را در MUD قرار می دهد، در حالی که تمام جنبه های مربوط به حریم خصوصی در Circom مدیریت می شوند.

یک پروژه MUD ایجاد کنید

pnpm create mud@latest tutorial
cd tutorial

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

1. دولت را تعریف کنید

موقعیت شخصیت‌ها در جداول MUD تعریف شده، عمومی نگه داشته می‌شوند. علاوه بر این، مقداری ZK اضافه می کنیم commitments برای اطمینان از اینکه هیچ کس نمی تواند تقلب کند، این بخش در بخش ZK آینده معنی بیشتری خواهد داشت.

packages/contracts/mud.config.ts

import { defineWorld } from “@latticexyz/world”;

export default defineWorld({
namespace: “app”,
enums: {
Direction: [
“Up”,
“Down”,
“Left”,
“Right”
] },
tables: {
Character: {
schema: {
x: “int32”,
y: “int32”,
owner: “address”,
id: “uint32”,
attackedAt: “uint32”,
attackedByValue: “uint32”,
revealedValue: “uint32”,
isDead: “bool”,
},
key: [“x”, “y”] },
PlayerPrivateState: {
schema: {
account: “address”,
commitment: “uint256”,
},
key: [“account”] },
VerifierContracts: {
schema: {
revealContractAddress: “address”,
defendContractAddress: “address”
},
key: [],
},
},
});

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

2. مدارهای رزمی را ایجاد کنید

مدارهای نبرد شامل SNARK حمله و SNARK دفاعی است.

الف به SNARK حمله کنید

وقتی یک شخصیت حمله می کند، نوع خود را آشکار می کند. برای اطمینان از اینکه بازیکن تقلب نمی کند، یک SNARK ایجاد می کنیم که انواع اولیه تعریف شده توسط بازیکن در شروع بازی را هش می کند. مدار تضمین می کند که بازیکن یک کاراکتر از هر نوع را اختصاص داده است و سپس همه چیز با a هش می شود privateSalt، که به عنوان یک کلید خصوصی عمل می کند و از حملات brute-force جلوگیری می کند تا وضعیت اولیه ای که بازیکن به آن متعهد شده است را آشکار کند. این هش a نامیده می شود commitment، که در یک جدول MUD ذخیره می شود که به تأیید اینکه همه چیز به درستی اتفاق افتاده است کمک می کند. جزئیات بیشتر در این مورد در بخش قراردادهای زیر پوشش داده خواهد شد.

packages/zk/circuits/reveal/reveal.circom

pragma circom 2.0.0;

include “../circomlib/circuits/poseidon.circom”;
include “../circomlib/circuits/comparators.circom”;

template spawn() {
// Input signals
signal input character1;
signal input character2;
signal input character3;
signal input character4;
signal input privateSalt;
signal input characterReveal; // The character index to reveal (1, 2, 3, or 4)
signal input valueReveal; // The value that is claimed to be assigned to the character

// Output signal for the hash
signal output hash;

// Poseidon hash calculation
component poseidonComponent = Poseidon(5);
poseidonComponent.inputs[0] character1;
poseidonComponent.inputs[1] character2;
poseidonComponent.inputs[2] character3;
poseidonComponent.inputs[3] character4;
poseidonComponent.inputs[4] privateSalt;
hash poseidonComponent.out;

// Comparator components for character reveal verification
component isChar1 = IsEqual();
component isChar2 = IsEqual();
component isChar3 = IsEqual();
component isChar4 = IsEqual();
isChar1.in[0] characterReveal;
isChar1.in[1] 1;
isChar2.in[0] characterReveal;
isChar2.in[1] 2;
isChar3.in[0] characterReveal;
isChar3.in[1] 3;
isChar4.in[0] characterReveal;
isChar4.in[1] 4;

// Value check depending on the revealed character
component checkChar1 = IsEqual();
component checkChar2 = IsEqual();
component checkChar3 = IsEqual();
component checkChar4 = IsEqual();

checkChar1.in[0] isChar1.out * character1 + (1 – isChar1.out) * 0;
checkChar1.in[1] valueReveal;

checkChar2.in[0] isChar2.out * character2 + (1 – isChar2.out) * 0;
checkChar2.in[1] valueReveal;

checkChar3.in[0] isChar3.out * character3 + (1 – isChar3.out) * 0;
checkChar3.in[1] valueReveal;

checkChar4.in[0] isChar4.out * character4 + (1 – isChar4.out) * 0;
checkChar4.in[1] valueReveal;

signal validReveal1;
signal validReveal2;
signal validReveal3;
signal validReveal4;

validReveal1 checkChar1.out;
validReveal2 checkChar2.out;
validReveal3 checkChar3.out;
validReveal4 checkChar4.out;

signal validReveal validReveal1 + validReveal2 + validReveal3 + validReveal4;
validReveal === 1;

// Comparators to check for presence of values 1, 2, 3, 4
component isOne1 = IsEqual();
component isOne2 = IsEqual();
component isOne3 = IsEqual();
component isOne4 = IsEqual();
isOne1.in[0] character1;
isOne1.in[1] 1;
isOne2.in[0] character2;
isOne2.in[1] 1;
isOne3.in[0] character3;
isOne3.in[1] 1;
isOne4.in[0] character4;
isOne4.in[1] 1;
signal oneExists isOne1.out + isOne2.out + isOne3.out + isOne4.out;
oneExists === 1;

component isTwo1 = IsEqual();
component isTwo2 = IsEqual();
component isTwo3 = IsEqual();
component isTwo4 = IsEqual();
isTwo1.in[0] character1;
isTwo1.in[1] 2;
isTwo2.in[0] character2;
isTwo2.in[1] 2;
isTwo3.in[0] character3;
isTwo3.in[1] 2;
isTwo4.in[0] character4;
isTwo4.in[1] 2;
signal twoExists isTwo1.out + isTwo2.out + isTwo3.out + isTwo4.out;
twoExists === 1;

component isThree1 = IsEqual();
component isThree2 = IsEqual();
component isThree3 = IsEqual();
component isThree4 = IsEqual();
isThree1.in[0] character1;
isThree1.in[1] 3;
isThree2.in[0] character2;
isThree2.in[1] 3;
isThree3.in[0] character3;
isThree3.in[1] 3;
isThree4.in[0] character4;
isThree4.in[1] 3;
signal threeExists isThree1.out + isThree2.out + isThree3.out + isThree4.out;
threeExists === 1;

component isFour1 = IsEqual();
component isFour2 = IsEqual();
component isFour3 = IsEqual();
component isFour4 = IsEqual();
isFour1.in[0] character1;
isFour1.in[1] 4;
isFour2.in[0] character2;
isFour2.in[1] 4;
isFour3.in[0] character3;
isFour3.in[1] 4;
isFour4.in[0] character4;
isFour4.in[1] 4;
signal fourExists isFour1.out + isFour2.out + isFour3.out + isFour4.out;
fourExists === 1;
}

component main {public [characterReveal, valueReveal]} = spawn();

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

ب SNARK دفاعی

هنگامی که یک شخصیت مورد حمله قرار می گیرد، وارد حالتی می شود که نمی تواند حرکت کند یا حمله کند. برای خروج از این حالت، شخصیت باید یک SNARK ارائه کند که ثابت کند دفاعش موفق بوده است بدون اینکه نوع آن مشخص شود. این از طریق مداری به دست می آید که دفاع را بر اساس نوع عمومی مهاجم و نوع خصوصی مدافع تأیید می کند.

packages/zk/circuits/defend/defend.circom

pragma circom 2.0.0;

include “../circomlib/circuits/poseidon.circom”;
include “../circomlib/circuits/comparators.circom”;

template CharacterBattleCheck() {
// Input signals
signal input character1;
signal input character2;
signal input character3;
signal input character4;
signal input privateSalt;
signal input characterTarget; // 1-based index: 1 for character1, 2 for character2, etc.
signal input attackerLevel;

// Output signal for the hash
signal output hash;

// Output signal for the battle result
signal output battleResult;

// Poseidon hash calculation
component poseidonComponent = Poseidon(5);
poseidonComponent.inputs[0] character1;
poseidonComponent.inputs[1] character2;
poseidonComponent.inputs[2] character3;
poseidonComponent.inputs[3] character4;
poseidonComponent.inputs[4] privateSalt;
hash poseidonComponent.out;

// Create binary indicators for each target
signal isTarget1;
signal isTarget2;
signal isTarget3;
signal isTarget4;

// Check if characterTarget matches 1, 2, 3, or 4
component isTarget1Eq = IsEqual();
isTarget1Eq.in[0] characterTarget;
isTarget1Eq.in[1] 1;
isTarget1 isTarget1Eq.out;

component isTarget2Eq = IsEqual();
isTarget2Eq.in[0] characterTarget;
isTarget2Eq.in[1] 2;
isTarget2 isTarget2Eq.out;

component isTarget3Eq = IsEqual();
isTarget3Eq.in[0] characterTarget;
isTarget3Eq.in[1] 3;
isTarget3 isTarget3Eq.out;

component isTarget4Eq = IsEqual();
isTarget4Eq.in[0] characterTarget;
isTarget4Eq.in[1] 4;
isTarget4 isTarget4Eq.out;

// Ensure exactly one of the targets is selected
signal sumTargets;
sumTargets isTarget1 + isTarget2 + isTarget3 + isTarget4;
sumTargets === 1;

// Use separate variables to hold the selected character values
signal selectedCharacter1;
signal selectedCharacter2;
signal selectedCharacter3;
signal selectedCharacter4;

// Enforce that only one of the selectedCharacter variables holds the value
selectedCharacter1 isTarget1 * character1;
selectedCharacter2 isTarget2 * character2;
selectedCharacter3 isTarget3 * character3;
selectedCharacter4 isTarget4 * character4;

// Aggregate the selected character value
signal selectedCharacter;
selectedCharacter selectedCharacter1 + selectedCharacter2 + selectedCharacter3 + selectedCharacter4;

// Compare attackerLevel and selectedCharacter
component compareLevel = LessThan(4); // Assuming levels are within 4 bits (0-15)
compareLevel.in[0] selectedCharacter;
compareLevel.in[1] attackerLevel;
signal attackerWinsNormal compareLevel.out;

// Special rule: attackerLevel == 1 and selectedCharacter == 4
component isAttackerLevelOneEq = IsEqual();
isAttackerLevelOneEq.in[0] attackerLevel;
isAttackerLevelOneEq.in[1] 1;
signal isAttackerLevelOne isAttackerLevelOneEq.out;

component isCharacterTargetFourEq = IsEqual();
isCharacterTargetFourEq.in[0] selectedCharacter;
isCharacterTargetFourEq.in[1] 4;
signal isCharacterTargetFour isCharacterTargetFourEq.out;

signal attackerWinsSpecial;
attackerWinsSpecial isAttackerLevelOne * isCharacterTargetFour;

// Determine if the attacker wins either normally or via special rule
signal attackerWins;
attackerWins attackerWinsNormal + attackerWinsSpecial;

// Convert attackerWins to a binary value (0 or 1)
signal isAttackerWins;
signal zeroFlag;
signal oneFlag;

// Determine zeroFlag: 1 if attackerWins == 0, else 0
zeroFlag attackerWins * (attackerWins – 1);
oneFlag 1 – zeroFlag;

// isAttackerWins should be 1 if attackerWins > 0, else 0
isAttackerWins attackerWins – zeroFlag;

// Calculate the battleResult: 1 if defender wins, 2 if attacker wins
signal defenderWins;
defenderWins 1 – isAttackerWins;

// Output battleResult: 1 if defender wins, 2 if attacker wins
battleResult 1 + isAttackerWins;

log(battleResult);
}

component main {public [characterTarget, attackerLevel]} = CharacterBattleCheck();

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

همانطور که می بینید، ما از کتابخانه های Poseidon و مقایسه کننده استفاده می کنیم، آنها را نصب کنید.

cd packages/zk/circuits
git clone https://github.com/iden3/circomlib.git

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

ج قراردادهای تأیید کننده را ایجاد کنید

وارد پوشه آشکار مدار شوید.

cd reveal

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

مدار را کامپایل کنید.

circom reveal.circom –r1cs –wasm –sym

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

قرارداد groth16 مراسم و تایید کننده را ایجاد کنید.

snarkjs powersoftau new bn128 12 pot12_0000.ptau -v
snarkjs powersoftau contribute pot12_0000.ptau pot12_0001.ptau –name=”First contribution” -v
snarkjs powersoftau prepare phase2 pot12_0001.ptau pot12_final.ptau -v
snarkjs groth16 setup reveal.r1cs pot12_final.ptau reveal_0000.zkey
snarkjs zkey contribute reveal_0000.zkey reveal_0001.zkey –name=”1st Contributor Name” -v
snarkjs zkey export verificationkey reveal_0001.zkey verification_key.json
snarkjs zkey export solidityverifier reveal_0001.zkey ../../../contracts/src/RevealVerifier.sol

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

آن را در پوشه قرارداد MUD قرار دهید.

mkdir ../../../client/public/zk_artifacts/
cp reveal_js/reveal.wasm ../../../client/public/zk_artifacts/
cp reveal_0001.zkey ../../../client/public/zk_artifacts/reveal_final.zkey

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

حالا همین کار را با مدار دفاعی انجام دهید.

cd ../defend

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

کامپایل.

circom defend.circom –r1cs –wasm –sym

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

و تایید کننده را تولید کنید.

snarkjs powersoftau new bn128 12 pot12_0000.ptau -v
snarkjs powersoftau contribute pot12_0000.ptau pot12_0001.ptau –name=”First contribution” -v
snarkjs powersoftau prepare phase2 pot12_0001.ptau pot12_final.ptau -v
snarkjs groth16 setup defend.r1cs pot12_final.ptau defend_0000.zkey
snarkjs zkey contribute defend_0000.zkey defend_0001.zkey –name=”1st Contributor Name” -v
snarkjs zkey export verificationkey defend_0001.zkey verification_key.json
snarkjs zkey export solidityverifier defend_0001.zkey ../../../contracts/src/DefendVerifier.sol

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

آن را در پوشه قراردادها قرار دهید.

mkdir ../../../client/public/zk_artifacts/
cp defend_js/defend.wasm ../../../client/public/zk_artifacts/
cp defend_0001.zkey ../../../client/public/zk_artifacts/defend_final.zkey

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

شما همچنین باید نام عمومی را تغییر دهید Groth16Verifier به RevealVerifier و DefendVerifierبه ترتیب در قراردادهایی که اخیراً در آن قرار دادیم packages/client/public/zk_artifacts/.

3. منطق بازی

چند فایل را که از آنها استفاده نخواهیم کرد حذف کنید.

cd ../../../../
rm packages/contracts/src/systems/IncrementSystem.sol packages/contracts/test/CounterTest.t.sol

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

تمام منطق بازی در Solidity تعریف شده است. این شامل مراحل اولیه تخم ریزی، به دنبال آن مرحله حمله و دفاع است که هر کدام دارای اثبات ZK مربوط به خود هستند. الف را نیز اضافه کردیم killUnresponsiveCharacter عملکرد، که اگر بازیکنی نتواند در یک زمان معین اثبات ZK دفاعی خود را ارائه کند، حذف می کند.

packages/contracts/src/systems/MyGameSystem.sol

// SPDX-License-Identifier: MIT
pragma solidity >=0.8.24;

import { System } from “@latticexyz/world/src/System.sol”;
import { Character, CharacterData, VerifierContracts } from “../codegen/index.sol”;
import { PlayerPrivateState } from “../codegen/index.sol”;
import { Direction } from “../codegen/common.sol”;
import { getKeysWithValue } from “@latticexyz/world-modules/src/modules/keyswithvalue/getKeysWithValue.sol”;

import { EncodedLengths, EncodedLengthsLib } from “@latticexyz/store/src/EncodedLengths.sol”;

interface ICircomRevealVerifier {
function verifyProof(uint[2] calldata _pA, uint[2][2] calldata _pB, uint[2] calldata _pC, uint[3] calldata _pubSignals) external view returns (bool);
}

interface ICircomDefendVerifier {
function verifyProof(uint[2] calldata _pA, uint[2][2] calldata _pB, uint[2] calldata _pC, uint[4] calldata _pubSignals) external view returns (bool);
}

contract MyGameSystem is System {
function spawn(int32 x, int32 y, uint256 commitment) public {
//require(PlayerPrivateState.getCommitment(_msgSender()) == 0, “Player already spawned”);

Character.set(x, y, _msgSender(), 1, 0, 0, 0, false);
Character.set(x, y + 1, _msgSender(), 2, 0, 0, 0, false);
Character.set(x, y + 2, _msgSender(), 3, 0, 0, 0, false);
Character.set(x, y + 3, _msgSender(), 4, 0, 0, 0, false);

PlayerPrivateState.set(_msgSender(), commitment);
}

function move(int32 characterAtX, int32 characterAtY, Direction direction) public {
CharacterData memory character = Character.get(characterAtX, characterAtY);

//require(!character.isDead, “Character is dead”);
require(character.attackedAt == 0, “Character is under attack”);
require(character.owner == _msgSender(), “Only owner”);

int32 x = characterAtX;
int32 y = characterAtY;

if(direction == Direction.Up)
y -= 1;
if(direction == Direction.Down)
y += 1;
if(direction == Direction.Left)
x -= 1;
if(direction == Direction.Right)
x += 1;

CharacterData memory characterAtDestination = Character.get(x, y);
require(characterAtDestination.owner == address(0), “Destination is occupied”);

Character.deleteRecord(characterAtX, characterAtY);
Character.set(x, y, _msgSender(), character.id, 0, 0, character.revealedValue, false);
}

function attack(uint[2] calldata _pA, uint[2][2] calldata _pB, uint[2] calldata _pC, uint[3] calldata _pubSignals,
int32 fromX, int32 fromY, int32 toX, int32 toY
) public {
ICircomRevealVerifier(VerifierContracts.getRevealContractAddress()).verifyProof(_pA, _pB, _pC, _pubSignals);
uint256 commitment = _pubSignals[0];
uint256 characterReveal = _pubSignals[1];
uint256 valueReveal = _pubSignals[2];

require(PlayerPrivateState.getCommitment(_msgSender()) == commitment, “Invalid commitment”);
require(characterReveal == Character.getId(fromX, fromY), “Invalid attacker id”);
require(Character.getOwner(fromX, fromY) == _msgSender(), “You’re not the planet owner”);
Character.setRevealedValue(fromX, fromY, uint32(valueReveal));
Character.setAttackedAt(toX, toY, uint32(block.timestamp));
Character.setAttackedByValue(toX, toY, uint32(valueReveal));
}

function defend(uint[2] calldata _pA, uint[2][2] calldata _pB, uint[2] calldata _pC, uint[4] calldata _pubSignals,
int32 x, int32 y
) public {
ICircomDefendVerifier(VerifierContracts.getDefendContractAddress()).verifyProof(_pA, _pB, _pC, _pubSignals);

uint256 commitment = _pubSignals[0];
uint256 battleResult = _pubSignals[1];
uint256 characterTarget = _pubSignals[2];
uint256 attackerLevel = _pubSignals[3];

require(PlayerPrivateState.getCommitment(Character.getOwner(x, y)) == commitment, “Invalid commitment”);
require(characterTarget == Character.getId(x, y), “Invalid character id”);
require(attackerLevel == Character.getAttackedByValue(x, y), “Invalid attacked by value in proof”);

if(battleResult == 1) { // defense won
Character.setAttackedAt(x, y, 0);
Character.setAttackedByValue(x, y, 0);
} else { // attack won
Character.setIsDead(x, y, true);
}
}

function killUnresponsiveCharacter(int32 x, int32 y) public {
uint32 attackedAt = Character.getAttackedAt(x, y);
uint32 MAX_WAIT_TIME = 1 minutes;
require(attackedAt>0 && (attackedAt – uint32(block.timestamp)) > MAX_WAIT_TIME, “Can kill character now”);
Character.setIsDead(x, y, true);
}
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

به یاد داشته باشید که در MUD، ما از سازنده های سنتی استفاده نمی کنیم. این به این دلیل است که یک مجرد System قرارداد می تواند وضعیت جهان های متعدد را مدیریت کند. در عوض، ما از PostDeploy قرارداد، جایی که ما قراردادهای تأیید کننده را مستقر می کنیم.

packages/contracts/script/PostDeploy.s.sol

// SPDX-License-Identifier: MIT
pragma solidity >=0.8.24;

import { Script } from “forge-std/Script.sol”;
import { console } from “forge-std/console.sol”;
import { StoreSwitch } from “@latticexyz/store/src/StoreSwitch.sol”;

import { RevealVerifier } from “../src/RevealVerifier.sol”;
import { DefendVerifier } from “../src/DefendVerifier.sol”;

import { IWorld } from “../src/codegen/world/IWorld.sol”;

import { VerifierContracts } from “../src/codegen/index.sol”;

contract PostDeploy is Script {
function run(address worldAddress) external {
// Specify a store so that you can use tables directly in PostDeploy
StoreSwitch.setStoreAddress(worldAddress);

// Load the private key from the `PRIVATE_KEY` environment variable (in .env)
uint256 deployerPrivateKey = vm.envUint(“PRIVATE_KEY”);

// Start broadcasting transactions from the deployer account
vm.startBroadcast(deployerPrivateKey);

address revealVerifier = address(new RevealVerifier());
VerifierContracts.setRevealContractAddress(revealVerifier);

address defendVerifier = address(new DefendVerifier());
VerifierContracts.setDefendContractAddress(defendVerifier);

vm.stopBroadcast();
}
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

4. مشتری با Phaser + Snarkjs

فایلی را ایجاد کنید که منطق رابط کاربری را مدیریت کند. این فایل اعمال ماوس را برای کشیدن به حرکت، اتصال دو کاراکتر به حمله و کلیک کردن برای دفاع را مدیریت می کند. علاوه بر این، منطق انیمیشن ها را تعریف می کند.

packages/client/src/layers/phaser/systems/myGameSystem.ts

import { Has, defineEnterSystem, defineExitSystem, defineSystem, getComponentValueStrict, getComponentValue } from “@latticexyz/recs”;
import { PhaserLayer } from “../createPhaserLayer”;
import {
pixelCoordToTileCoord,
tileCoordToPixelCoord
} from “@latticexyz/phaserx”;
import { TILE_WIDTH, TILE_HEIGHT, Animations, Directions } from “../constants”;

function decodePosition(hexString) {
if (hexString.startsWith(‘0x’)) {
hexString = hexString.slice(2);
}

const halfLength = hexString.length / 2;
const firstHalfHex = hexString.slice(0, halfLength);
const secondHalfHex = hexString.slice(halfLength);

const firstHalfInt32 = getSignedInt32(firstHalfHex);
const secondHalfInt32 = getSignedInt32(secondHalfHex);

return { x: firstHalfInt32, y: secondHalfInt32 };
}

function getSignedInt32(hexStr) {
const int32Value = parseInt(hexStr.slice(-8), 16);

if (int32Value > 0x7FFFFFFF) {
return int32Value – 0x100000000;
}
return int32Value;
}

function encodePosition(x: number, y: number): string {
const xHex = int256ToHex(x);
const yHex = int256ToHex(y);

// Concatenate the two 32-byte hex values to form a 64-byte hex string
return ‘0x’ + xHex + yHex;
}

function int256ToHex(value: number): string {
// If the value is negative, convert it to a 256-bit unsigned integer
if (value 0) {
value = BigInt(‘0x10000000000000000000000000000000000000000000000000000000000000000’) + BigInt(value);
} else {
value = BigInt(value);
}

// Convert the integer to a hexadecimal string, ensuring it has 64 characters (256 bits)
let hexStr = value.toString(16);
while (hexStr.length 64) {
hexStr = ‘0’ + hexStr;
}

return hexStr;
}

export const createMyGameSystem = (layer: PhaserLayer) => {
const {
world,
networkLayer: {
components: { Character },
systemCalls: { spawn, move, attack, defend, playerEntity }
},
scenes: {
Main: { objectPool, input }
}
} = layer;

let startPoint: { x: number; y: number } | null = null;
let draggedEntity: string | null = null;

let playerRectangle1 = objectPool.get(“PlayerRectangle1”, “Rectangle”);
let playerRectangle2 = objectPool.get(“PlayerRectangle2”, “Rectangle”);
let playerRectangle3 = objectPool.get(“PlayerRectangle3”, “Rectangle”);
let playerRectangle4 = objectPool.get(“PlayerRectangle4”, “Rectangle”);
let arrowLine1 = objectPool.get(“ArrowLine1”, “Line”);
let arrowLine2 = objectPool.get(“ArrowLine2”, “Line”);
let arrowLine3 = objectPool.get(“ArrowLine3”, “Line”);

let secretCharacterValues = [0, 4, 1, 2, 3];
let privateSalt = 123;

input.pointerdown$.subscribe((event) => {
const { worldX, worldY } = event.pointer;
const player = pixelCoordToTileCoord({ x: worldX, y: worldY }, TILE_WIDTH, TILE_HEIGHT);

if (player.x === 0 && player.y === 0) return;

let coordinates = pixelCoordToTileCoord({ x: worldX, y: worldY }, TILE_WIDTH, TILE_HEIGHT);
let encodedPosition = encodePosition(coordinates.x, coordinates.y);

const character = getComponentValue(Character, encodedPosition);

if (character) {
startPoint = { x: worldX, y: worldY };
draggedEntity = `${player.x}-${player.y}`;
} else {
spawn(player.x, player.y, 123);
}
});

input.pointermove$.subscribe((event) => {
if (startPoint && draggedEntity) {
const { worldX, worldY } = event.pointer;

// Draw the main line
arrowLine1.setComponent({
id: “line”,
once: (line) => {
line.visible = true;
line.isStroked = true;
line.setFillStyle(0xff00ff);
line.geom.x1 = startPoint.x;
line.geom.y1 = startPoint.y;
line.geom.x2 = worldX;
line.geom.y2 = worldY;
},
});

// Draw an arrowhead effect at the end point
const arrowLength = 20;
const angle = Math.atan2(worldY – startPoint.y, worldX – startPoint.x);

arrowLine2.setComponent({
id: “line”,
once: (line) => {
line.visible = true;
line.isStroked = true;
line.setFillStyle(0x00ff00);
line.geom.x1 = worldX;
line.geom.y1 = worldY;
line.geom.x2 = worldX – arrowLength * Math.cos(angle – Math.PI / 6);
line.geom.y2 = worldY – arrowLength * Math.sin(angle – Math.PI / 6);
},
});

arrowLine3.setComponent({
id: “line”,
once: (line) => {
line.visible = true;
line.isStroked = true;
line.setFillStyle(0x00ff00);
line.geom.x1 = worldX;
line.geom.y1 = worldY;
line.geom.x2 = worldX – arrowLength * Math.cos(angle + Math.PI / 6);
line.geom.y2 = worldY – arrowLength * Math.sin(angle + Math.PI / 6);
},
});
}
});

input.pointerup$.subscribe((event) => {
if (startPoint && draggedEntity) {

const { worldX, worldY } = event.pointer;

const startTile = pixelCoordToTileCoord(startPoint, TILE_WIDTH, TILE_HEIGHT);
const endTile = pixelCoordToTileCoord({ x: worldX, y: worldY }, TILE_WIDTH, TILE_HEIGHT);

const encodedDestinationPosition = encodePosition(endTile.x, endTile.y);
const destinationCharacter = getComponentValue(Character, encodedDestinationPosition);
const direction = calculateDirection(startTile, endTile);

if(startTile.x == endTile.x
&& startTile.y == endTile.y)
{

console.log(`Defending character at (${startTile.x}, ${startTile.y})`);

defend(startTile.x, startTile.y,
{
character1: secretCharacterValues[1],
character2: secretCharacterValues[2],
character3: secretCharacterValues[3],
character4: secretCharacterValues[4],
privateSalt: privateSalt,
characterTarget: destinationCharacter.id,
attackerLevel: destinationCharacter.attackedByValue
});

} else if (destinationCharacter) {
const encodedStartPosition = encodePosition(startTile.x, startTile.y);
const startCharacter = getComponentValue(Character, encodedStartPosition);

attack(startTile.x, startTile.y, endTile.x, endTile.y,
{
character1: secretCharacterValues[1],
character2: secretCharacterValues[2],
character3: secretCharacterValues[3],
character4: secretCharacterValues[4],
privateSalt: privateSalt,
characterReveal: startCharacter.id,
valueReveal: secretCharacterValues[startCharacter.id] }
);
console.log(`Attacked character from (${startTile.x}, ${startTile.y}) to (${endTile.x}, ${endTile.y})`);
} else if (direction != null) {
move(startTile.x, startTile.y, direction);
console.log(`Moved character from (${startTile.x}, ${startTile.y}) to (${endTile.x}, ${endTile.y}) in direction ${direction}`);
}

startPoint = null;
draggedEntity = null;
}
});

defineEnterSystem(world, [Has(Character)], ({ entity }) => {
const character = getComponentValue(Character, entity);

const characterObj = objectPool.get(entity, “Sprite”);
characterObj.setComponent({
id: ‘animation’,
once: (sprite) => {
let characterAnimation = character.revealedValue;
const playerIsOwner = “0x” + playerEntity.slice(26).toLowerCase() == “” + character.owner.toLowerCase()
if(playerIsOwner) {
characterAnimation = secretCharacterValues[character.id];
}
if(playerIsOwner) {
const characterPosition = tileCoordToPixelCoord(decodePosition(entity), TILE_WIDTH, TILE_HEIGHT);
let rectangle = null

switch (character.id) {
case 1:
rectangle = playerRectangle1;
break;
case 2:
rectangle = playerRectangle2;
break;
case 3:
rectangle = playerRectangle3;
break;
case 4:
rectangle = playerRectangle4;
break;
}

rectangle.setComponent({
id: “rectangle”,
once: (rectangle) => {
rectangle.setPosition(characterPosition.x, characterPosition.y);
rectangle.setSize(32,32);
rectangle.setFillStyle(0x0000ff);
rectangle.setAlpha(0.25);
},
});

}
switch (characterAnimation) {
case 1:
sprite.play(Animations.A);
break;
case 2:
sprite.play(Animations.B);
break;
case 3:
sprite.play(Animations.C);
break;
case 4:
sprite.play(Animations.D);
break;
default:
sprite.play(Animations.Unknown);
}
}
});
});

defineExitSystem(world, [Has(Character)], ({ entity }) => {
objectPool.remove(entity);
});

defineSystem(world, [Has(Character)], ({ entity }) => {
const character = getComponentValue(Character, entity);
if(!character)
return;
const pixelPosition = tileCoordToPixelCoord(decodePosition(entity), TILE_WIDTH, TILE_HEIGHT);
const characterObj = objectPool.get(entity, “Sprite”);

if (character.isDead) {
characterObj.setComponent({
id: ‘animation’,
once: (sprite) => {
sprite.play(Animations.Dead);
}
});
} else if (character.attackedAt != 0) {
characterObj.setComponent({
id: ‘animation’,
once: (sprite) => {
sprite.play(Animations.Attacked);
}
});
} else
{
characterObj.setComponent({
id: ‘animation’,
once: (sprite) => {
let characterAnimation = character.revealedValue;
const playerIsOwner = “0x” + playerEntity.slice(26).toLowerCase() == “” + character.owner.toLowerCase()
if(playerIsOwner) {
characterAnimation = secretCharacterValues[character.id];
}
if(playerIsOwner) {
//sprite.setBackgroundColor(“#0000ff”);
}
switch (characterAnimation) {
case 1:
sprite.play(Animations.A);
break;
case 2:
sprite.play(Animations.B);
break;
case 3:
sprite.play(Animations.C);
break;
case 4:
sprite.play(Animations.D);
break;
default:
sprite.play(Animations.Unknown);
}
}
});
}

characterObj.setComponent({
id: “position”,
once: (sprite) => {
sprite.setPosition(pixelPosition.x, pixelPosition.y);
}
});

arrowLine1.setComponent({
id: “line”,
once: (line) => {
line.visible = false;
},
});

arrowLine2.setComponent({
id: “line”,
once: (line) => {
line.visible = false;
},
});

arrowLine3.setComponent({
id: “line”,
once: (line) => {
line.visible = false;
},
});
});

function calculateDirection(start: { x: number, y: number }, end: { x: number, y: number }) {
if (end.y start.y) return Directions.UP;
if (end.y > start.y) return Directions.DOWN;
if (end.x start.x) return Directions.LEFT;
if (end.x > start.x) return Directions.RIGHT;
return null;
}
};

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

5. تعامل Client-Ethereum و Client-SNARK

ابتدا باید کتابخانه ای را نصب کنیم که به ما در تولید SNARK کمک کند.

cd packages/client/
pnpm install snarkjs

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اکنون می‌توانیم تراکنش‌های زنجیره‌ای و تولید اثبات‌های ZK را تعریف کنیم.

packages/client/src/mud/createSystemCalls.ts

import { getComponentValue } from “@latticexyz/recs”;
import { ClientComponents } from “./createClientComponents”;
import { SetupNetworkResult } from “./setupNetwork”;
import { singletonEntity } from “@latticexyz/store-sync/recs”;
import { groth16 } from “snarkjs”;

export type SystemCalls = ReturnTypetypeof createSystemCalls>;

export function createSystemCalls(
{ worldContract, waitForTransaction, playerEntity }: SetupNetworkResult,
{ Character }: ClientComponents,
) {
const spawn = async (x: number, y: number) => {
const { proof, publicSignals } = await groth16.fullProve(
{
character1: 4,
character2: 1,
character3: 2,
character4: 3,
privateSalt: 123,
characterReveal: 1,
valueReveal: 4,
},
“./zk_artifacts/reveal.wasm”,
“./zk_artifacts/reveal_final.zkey”
);
let commitment : number = publicSignals[0];
const tx = await worldContract.write.app__spawn([x, y, commitment]);
await waitForTransaction(tx);
return getComponentValue(Character, singletonEntity);
};

const move = async (x: number, y: number, direction: number) => {
const tx = await worldContract.write.app__move([x, y, direction]);
await waitForTransaction(tx);
return getComponentValue(Character, singletonEntity);
}

const attack = async (fromX: number, fromY: number, toX: number, toY: number, circuitInputs: any) => {
const { proof, publicSignals } = await groth16.fullProve(circuitInputs,
“./zk_artifacts/reveal.wasm”,
“./zk_artifacts/reveal_final.zkey”
);

let pa = proof.pi_a
let pb = proof.pi_b
let pc = proof.pi_c
pa.pop()
pb.pop()
pc.pop()

const tx = await worldContract.write.app__attack([pa, pb, pc, publicSignals, fromX, fromY, toX, toY]);
await waitForTransaction(tx);
return getComponentValue(Character, singletonEntity);
}

const defend = async (x: number, y: number, circuitInputs: any) => {
const { proof, publicSignals } = await groth16.fullProve(circuitInputs,
“./zk_artifacts/defend.wasm”,
“./zk_artifacts/defend_final.zkey”
);

let pa = proof.pi_a
let pb = proof.pi_b
let pc = proof.pi_c
pa.pop()
pb.pop()
pc.pop()

const tx = await worldContract.write.app__defend([pa, pb, pc, publicSignals, x, y]);
await waitForTransaction(tx);
return getComponentValue(Character, singletonEntity);
}

return {
spawn, move, attack, defend, playerEntity
};
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

6. اضافه کردن انیمیشن های بازی

می‌توانید از هر انیمیشنی که دوست دارید استفاده کنید، اما اگر می‌خواهید از همان انیمیشن‌هایی که من استفاده می‌کنم استفاده کنید، می‌توانید این دارایی‌ها را دانلود کنید:

packages/art/sprites/A/1.png

packages/art/sprites/B/1.png

packages/art/sprites/C/1.png

packages/art/sprites/D/1.png

packages/art/sprites/Attacked/1.png

packages/art/sprites/Dead/1.png

packages/art/sprites/Unknown/1.png

اطلس را تولید کنید.

cd packages/art
yarn
yarn generate-multiatlas-sprites

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

و انیمیشن های بازی را تعریف می کنیم. در اینجا می توانید انیمیشن های چند فریمی اضافه کنید و رفتار و سرعت آنها را تنظیم کنید.

انیمیشن های بازی را تعریف کنید. می توانید شهرت، سرعت و رفتار را تنظیم کنید.

packages/client/src/layers/phaser/configurePhaser.ts

import Phaser from “phaser”;
import {
defineSceneConfig,
AssetType,
defineScaleConfig,
defineMapConfig,
defineCameraConfig,
} from “@latticexyz/phaserx”;
import worldTileset from “../../../public/assets/tilesets/world.png”;
import { TileAnimations, Tileset } from “../../artTypes/world”;
import { Assets, Maps, Scenes, TILE_HEIGHT, TILE_WIDTH, Animations } from “./constants”;

const ANIMATION_INTERVAL = 200;

const mainMap = defineMapConfig({
chunkSize: TILE_WIDTH * 64, // tile size * tile amount
tileWidth: TILE_WIDTH,
tileHeight: TILE_HEIGHT,
backgroundTile: [Tileset.Grass],
animationInterval: ANIMATION_INTERVAL,
tileAnimations: TileAnimations,
layers: {
layers: {
Background: { tilesets: [“Default”] },
Foreground: { tilesets: [“Default”] },
},
defaultLayer: “Background”,
},
});

export const phaserConfig = {
sceneConfig: {
[Scenes.Main]: defineSceneConfig({
assets: {
[Assets.Tileset]: {
type: AssetType.Image,
key: Assets.Tileset,
path: worldTileset,
},
[Assets.MainAtlas]: {
type: AssetType.MultiAtlas,
key: Assets.MainAtlas,
// Add a timestamp to the end of the path to prevent caching
path: `/assets/atlases/atlas.json?timestamp=${Date.now()}`,
options: {
imagePath: “/assets/atlases/”,
},
},
},
maps: {
[Maps.Main]: mainMap,
},
sprites: {
},
animations: [
{
key: Animations.A,
assetKey: Assets.MainAtlas,
startFrame: 1,
endFrame: 1,
frameRate: 3,
repeat: -1,
duration: 1,
prefix: “sprites/A/”,
suffix: “.png”,
},
{
key: Animations.B,
assetKey: Assets.MainAtlas,
startFrame: 1,
endFrame: 1,
frameRate: 3,
repeat: -1,
duration: 1,
prefix: “sprites/B/”,
suffix: “.png”,
},
{
key: Animations.C,
assetKey: Assets.MainAtlas,
startFrame: 1,
endFrame: 1,
frameRate: 3,
repeat: -1,
duration: 1,
prefix: “sprites/C/”,
suffix: “.png”,
},
{
key: Animations.D,
assetKey: Assets.MainAtlas,
startFrame: 1,
endFrame: 1,
frameRate: 3,
repeat: -1,
duration: 1,
prefix: “sprites/D/”,
suffix: “.png”,
},
{
key: Animations.Dead,
assetKey: Assets.MainAtlas,
startFrame: 1,
endFrame: 1,
frameRate: 12,
repeat: -1,
duration: 1,
prefix: “sprites/Dead/”,
suffix: “.png”,
},
{
key: Animations.Unknown,
assetKey: Assets.MainAtlas,
startFrame: 1,
endFrame: 1,
frameRate: 12,
repeat: -1,
duration: 1,
prefix: “sprites/Unknown/”,
suffix: “.png”,
},
{
key: Animations.Attacked,
assetKey: Assets.MainAtlas,
startFrame: 1,
endFrame: 1,
frameRate: 12,
repeat: -1,
duration: 1,
prefix: “sprites/Attacked/”,
suffix: “.png”,
},
],
tilesets: {
Default: {
assetKey: Assets.Tileset,
tileWidth: TILE_WIDTH,
tileHeight: TILE_HEIGHT,
},
},
}),
},
scale: defineScaleConfig({
parent: “phaser-game”,
zoom: 1,
mode: Phaser.Scale.NONE,
}),
cameraConfig: defineCameraConfig({
pinchSpeed: 1,
wheelSpeed: 1,
maxZoom: 3,
minZoom: 1,
}),
cullingChunkSize: TILE_HEIGHT * 16,
};

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

7. همه چیز را به هم متصل کنید

packages/client/src/layers/phaser/constants.ts

export enum Scenes {
Main = “Main”,
}

export enum Maps {
Main = “Main”,
}

export enum Animations {
A = “A”,
B = “B”,
C = “C”,
D = “D”,
Dead = “Dead”,
Unknown = “Unknown”,
Attacked = “Attacked”,
}

export enum Directions {
UP = 0,
DOWN = 1,
LEFT = 2,
RIGHT = 3,
}

export enum Assets {
MainAtlas = “MainAtlas”,
Tileset = “Tileset”,
}

export const TILE_HEIGHT = 32;
export const TILE_WIDTH = 32;

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

packages/client/src/layers/phaser/systems/registerSystems.ts

import { PhaserLayer } from “../createPhaserLayer”;
import { createCamera } from “./createCamera”;
import { createMapSystem } from “./createMapSystem”;
import { createMyGameSystem } from “./myGameSystem”;

export const registerSystems = (layer: PhaserLayer) => {
createCamera(layer);
createMapSystem(layer);
createMyGameSystem(layer);
};

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

8. بازی را اجرا کنید

به فهرست اصلی پروژه برگردید.

cd ../../

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

و بازی را اجرا کنید.

pnpm dev

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اکنون می توانید بازی را در دو مرورگر مختلف باز کنید. هر بازیکن می تواند با کلیک بر روی یک فضای خالی تخم ریزی کند. برای حرکت به فضای خالی مجاور بکشید. برای حمله به بازیکن دیگری بکشید. برای ایجاد SNARK دفاعی روی بازیکن مورد حمله کلیک کنید.

در بازی برای تخم ریزی کلیک کنید، برای حرکت به فضای خالی مجاور بکشید، برای حمله به سمت حریف بکشید و برای دفاع کلیک کنید.

با تشکر از خواندن این راهنما!

من را در dev.to و YouTube برای همه چیزهایی که به توسعه بلاک چین به زبان اسپانیایی مرتبط است دنبال کنید.

در این آموزش، ما یک بازی 100% روی زنجیره با عدم تقارن اطلاعاتی ایجاد خواهیم کرد، به عبارت دیگر، یک دنیای مستقل که حالت خصوصی و محاسبات آن در اتریوم کاملاً قابل تأیید است. ما از MUD، موتور جهان های خودمختار، و Circom، پرکاربردترین زبان برای مدارهای ZK استفاده خواهیم کرد.

شما یاد خواهید گرفت که:

  • زبان Circom را با چارچوب MUD ترکیب کنید
  • یک دنیای مستقل با محاسبات و متغیرهای خصوصی ایجاد کنید
  • اثبات های zk-SNARK را مستقیماً از مرورگر خود با استفاده از Snark.js ایجاد کنید

فهرست مطالب

بازی

در شروع بازی، هر بازیکن 4 واحد تولید می کند که می تواند در نقشه حرکت کند. هر واحد از نوع متفاوتی است، اما این اطلاعات خصوصی است و فقط برای صاحب واحدها قابل مشاهده است.

منطق بازی

🐉 که 🧙 را می زند که 🧌 را می زند که 🗡️ را می زند. اما 🗡️ یک واحد ویژه است زیرا تنها واحدی است که می تواند 🐉 را شکست دهد

چهار نوع تعریف شده در بازی عبارتند از 🐉، که 🧙 را شکست می دهد، که 🧌 را شکست می دهد، که 🗡️ را شکست می دهد. با این حال، 🗡️ یک واحد ویژه است زیرا تنها واحدی است که می تواند 🐉 را شکست دهد. نبردها با استفاده از یک zk-SNARK انجام می شود که نوع مهاجم را نشان می دهد و به دنبال آن zk-SNARK دیگری که نتیجه نبرد را بدون مشخص کردن نوع واحد مورد حمله محاسبه می کند.

معماری بازی
معماری بازی تمام داده های عمومی و منطق را در MUD قرار می دهد، در حالی که تمام جنبه های مربوط به حریم خصوصی در Circom مدیریت می شوند.

یک پروژه MUD ایجاد کنید

pnpm create mud@latest tutorial
cd tutorial
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

1. دولت را تعریف کنید

موقعیت شخصیت‌ها در جداول MUD تعریف شده، عمومی نگه داشته می‌شوند. علاوه بر این، مقداری ZK اضافه می کنیم commitments برای اطمینان از اینکه هیچ کس نمی تواند تقلب کند، این بخش در بخش ZK آینده معنی بیشتری خواهد داشت.

packages/contracts/mud.config.ts

import { defineWorld } from "@latticexyz/world";

export default defineWorld({
  namespace: "app",
  enums: {
    Direction: [
      "Up",
      "Down",
      "Left",
      "Right"
    ]
  },
  tables: {
    Character: {
      schema: {
        x: "int32",
        y: "int32",
        owner: "address",
        id: "uint32",
        attackedAt: "uint32",
        attackedByValue: "uint32",
        revealedValue: "uint32",
        isDead: "bool",
      },
      key: ["x", "y"]
    },
    PlayerPrivateState: {
      schema: {
        account: "address",
        commitment: "uint256",
      },
      key: ["account"]
    },
    VerifierContracts: {
      schema: {
        revealContractAddress: "address",
        defendContractAddress: "address"
      },
      key: [],
    },
  },
});
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

2. مدارهای رزمی را ایجاد کنید

مدارهای نبرد شامل SNARK حمله و SNARK دفاعی است.

الف به SNARK حمله کنید

وقتی یک شخصیت حمله می کند، نوع خود را آشکار می کند. برای اطمینان از اینکه بازیکن تقلب نمی کند، یک SNARK ایجاد می کنیم که انواع اولیه تعریف شده توسط بازیکن در شروع بازی را هش می کند. مدار تضمین می کند که بازیکن یک کاراکتر از هر نوع را اختصاص داده است و سپس همه چیز با a هش می شود privateSalt، که به عنوان یک کلید خصوصی عمل می کند و از حملات brute-force جلوگیری می کند تا وضعیت اولیه ای که بازیکن به آن متعهد شده است را آشکار کند. این هش a نامیده می شود commitment، که در یک جدول MUD ذخیره می شود که به تأیید اینکه همه چیز به درستی اتفاق افتاده است کمک می کند. جزئیات بیشتر در این مورد در بخش قراردادهای زیر پوشش داده خواهد شد.

packages/zk/circuits/reveal/reveal.circom

pragma circom 2.0.0;

include "../circomlib/circuits/poseidon.circom";
include "../circomlib/circuits/comparators.circom";

template spawn() {
    // Input signals
    signal input character1;
    signal input character2;
    signal input character3;
    signal input character4;
    signal input privateSalt;
    signal input characterReveal; // The character index to reveal (1, 2, 3, or 4)
    signal input valueReveal; // The value that is claimed to be assigned to the character

    // Output signal for the hash
    signal output hash;

    // Poseidon hash calculation
    component poseidonComponent = Poseidon(5);
    poseidonComponent.inputs[0]  character1;
    poseidonComponent.inputs[1]  character2;
    poseidonComponent.inputs[2]  character3;
    poseidonComponent.inputs[3]  character4;
    poseidonComponent.inputs[4]  privateSalt;
    hash  poseidonComponent.out;

    // Comparator components for character reveal verification
    component isChar1 = IsEqual();
    component isChar2 = IsEqual();
    component isChar3 = IsEqual();
    component isChar4 = IsEqual();
    isChar1.in[0]  characterReveal;
    isChar1.in[1]  1;
    isChar2.in[0]  characterReveal;
    isChar2.in[1]  2;
    isChar3.in[0]  characterReveal;
    isChar3.in[1]  3;
    isChar4.in[0]  characterReveal;
    isChar4.in[1]  4;

    // Value check depending on the revealed character
    component checkChar1 = IsEqual();
    component checkChar2 = IsEqual();
    component checkChar3 = IsEqual();
    component checkChar4 = IsEqual();

    checkChar1.in[0]  isChar1.out * character1 + (1 - isChar1.out) * 0;
    checkChar1.in[1]  valueReveal;

    checkChar2.in[0]  isChar2.out * character2 + (1 - isChar2.out) * 0;
    checkChar2.in[1]  valueReveal;

    checkChar3.in[0]  isChar3.out * character3 + (1 - isChar3.out) * 0;
    checkChar3.in[1]  valueReveal;

    checkChar4.in[0]  isChar4.out * character4 + (1 - isChar4.out) * 0;
    checkChar4.in[1]  valueReveal;

    signal validReveal1;
    signal validReveal2;
    signal validReveal3;
    signal validReveal4;

    validReveal1  checkChar1.out;
    validReveal2  checkChar2.out;
    validReveal3  checkChar3.out;
    validReveal4  checkChar4.out;

    signal validReveal  validReveal1 + validReveal2 + validReveal3 + validReveal4;
    validReveal === 1;

    // Comparators to check for presence of values 1, 2, 3, 4
    component isOne1 = IsEqual();
    component isOne2 = IsEqual();
    component isOne3 = IsEqual();
    component isOne4 = IsEqual();
    isOne1.in[0]  character1;
    isOne1.in[1]  1;
    isOne2.in[0]  character2;
    isOne2.in[1]  1;
    isOne3.in[0]  character3;
    isOne3.in[1]  1;
    isOne4.in[0]  character4;
    isOne4.in[1]  1;
    signal oneExists  isOne1.out + isOne2.out + isOne3.out + isOne4.out;
    oneExists === 1;

    component isTwo1 = IsEqual();
    component isTwo2 = IsEqual();
    component isTwo3 = IsEqual();
    component isTwo4 = IsEqual();
    isTwo1.in[0]  character1;
    isTwo1.in[1]  2;
    isTwo2.in[0]  character2;
    isTwo2.in[1]  2;
    isTwo3.in[0]  character3;
    isTwo3.in[1]  2;
    isTwo4.in[0]  character4;
    isTwo4.in[1]  2;
    signal twoExists  isTwo1.out + isTwo2.out + isTwo3.out + isTwo4.out;
    twoExists === 1;

    component isThree1 = IsEqual();
    component isThree2 = IsEqual();
    component isThree3 = IsEqual();
    component isThree4 = IsEqual();
    isThree1.in[0]  character1;
    isThree1.in[1]  3;
    isThree2.in[0]  character2;
    isThree2.in[1]  3;
    isThree3.in[0]  character3;
    isThree3.in[1]  3;
    isThree4.in[0]  character4;
    isThree4.in[1]  3;
    signal threeExists  isThree1.out + isThree2.out + isThree3.out + isThree4.out;
    threeExists === 1;

    component isFour1 = IsEqual();
    component isFour2 = IsEqual();
    component isFour3 = IsEqual();
    component isFour4 = IsEqual();
    isFour1.in[0]  character1;
    isFour1.in[1]  4;
    isFour2.in[0]  character2;
    isFour2.in[1]  4;
    isFour3.in[0]  character3;
    isFour3.in[1]  4;
    isFour4.in[0]  character4;
    isFour4.in[1]  4;
    signal fourExists  isFour1.out + isFour2.out + isFour3.out + isFour4.out;
    fourExists === 1;
}

component main {public [characterReveal, valueReveal]} = spawn();
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

ب SNARK دفاعی

هنگامی که یک شخصیت مورد حمله قرار می گیرد، وارد حالتی می شود که نمی تواند حرکت کند یا حمله کند. برای خروج از این حالت، شخصیت باید یک SNARK ارائه کند که ثابت کند دفاعش موفق بوده است بدون اینکه نوع آن مشخص شود. این از طریق مداری به دست می آید که دفاع را بر اساس نوع عمومی مهاجم و نوع خصوصی مدافع تأیید می کند.

packages/zk/circuits/defend/defend.circom

pragma circom 2.0.0;

include "../circomlib/circuits/poseidon.circom";
include "../circomlib/circuits/comparators.circom";

template CharacterBattleCheck() {
    // Input signals
    signal input character1;
    signal input character2;
    signal input character3;
    signal input character4;
    signal input privateSalt;
    signal input characterTarget; // 1-based index: 1 for character1, 2 for character2, etc.
    signal input attackerLevel;

    // Output signal for the hash
    signal output hash;

    // Output signal for the battle result
    signal output battleResult;

    // Poseidon hash calculation
    component poseidonComponent = Poseidon(5);
    poseidonComponent.inputs[0]  character1;
    poseidonComponent.inputs[1]  character2;
    poseidonComponent.inputs[2]  character3;
    poseidonComponent.inputs[3]  character4;
    poseidonComponent.inputs[4]  privateSalt;
    hash  poseidonComponent.out;

    // Create binary indicators for each target
    signal isTarget1;
    signal isTarget2;
    signal isTarget3;
    signal isTarget4;

    // Check if characterTarget matches 1, 2, 3, or 4
    component isTarget1Eq = IsEqual();
    isTarget1Eq.in[0]  characterTarget;
    isTarget1Eq.in[1]  1;
    isTarget1  isTarget1Eq.out;

    component isTarget2Eq = IsEqual();
    isTarget2Eq.in[0]  characterTarget;
    isTarget2Eq.in[1]  2;
    isTarget2  isTarget2Eq.out;

    component isTarget3Eq = IsEqual();
    isTarget3Eq.in[0]  characterTarget;
    isTarget3Eq.in[1]  3;
    isTarget3  isTarget3Eq.out;

    component isTarget4Eq = IsEqual();
    isTarget4Eq.in[0]  characterTarget;
    isTarget4Eq.in[1]  4;
    isTarget4  isTarget4Eq.out;

    // Ensure exactly one of the targets is selected
    signal sumTargets;
    sumTargets  isTarget1 + isTarget2 + isTarget3 + isTarget4;
    sumTargets === 1;

    // Use separate variables to hold the selected character values
    signal selectedCharacter1;
    signal selectedCharacter2;
    signal selectedCharacter3;
    signal selectedCharacter4;

    // Enforce that only one of the selectedCharacter variables holds the value
    selectedCharacter1  isTarget1 * character1;
    selectedCharacter2  isTarget2 * character2;
    selectedCharacter3  isTarget3 * character3;
    selectedCharacter4  isTarget4 * character4;

    // Aggregate the selected character value
    signal selectedCharacter;
    selectedCharacter  selectedCharacter1 + selectedCharacter2 + selectedCharacter3 + selectedCharacter4;

    // Compare attackerLevel and selectedCharacter
    component compareLevel = LessThan(4); // Assuming levels are within 4 bits (0-15)
    compareLevel.in[0]  selectedCharacter;
    compareLevel.in[1]  attackerLevel;
    signal attackerWinsNormal  compareLevel.out;

    // Special rule: attackerLevel == 1 and selectedCharacter == 4
    component isAttackerLevelOneEq = IsEqual();
    isAttackerLevelOneEq.in[0]  attackerLevel;
    isAttackerLevelOneEq.in[1]  1;
    signal isAttackerLevelOne  isAttackerLevelOneEq.out;

    component isCharacterTargetFourEq = IsEqual();
    isCharacterTargetFourEq.in[0]  selectedCharacter;
    isCharacterTargetFourEq.in[1]  4;
    signal isCharacterTargetFour  isCharacterTargetFourEq.out;

    signal attackerWinsSpecial;
    attackerWinsSpecial  isAttackerLevelOne * isCharacterTargetFour;

    // Determine if the attacker wins either normally or via special rule
    signal attackerWins;
    attackerWins  attackerWinsNormal + attackerWinsSpecial;

    // Convert attackerWins to a binary value (0 or 1)
    signal isAttackerWins;
    signal zeroFlag;
    signal oneFlag;

    // Determine zeroFlag: 1 if attackerWins == 0, else 0
    zeroFlag  attackerWins * (attackerWins - 1);
    oneFlag  1 - zeroFlag;

    // isAttackerWins should be 1 if attackerWins > 0, else 0
    isAttackerWins  attackerWins - zeroFlag;

    // Calculate the battleResult: 1 if defender wins, 2 if attacker wins
    signal defenderWins;
    defenderWins  1 - isAttackerWins;

    // Output battleResult: 1 if defender wins, 2 if attacker wins
    battleResult  1 + isAttackerWins;

    log(battleResult);
}

component main {public [characterTarget, attackerLevel]} = CharacterBattleCheck();
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

همانطور که می بینید، ما از کتابخانه های Poseidon و مقایسه کننده استفاده می کنیم، آنها را نصب کنید.

cd packages/zk/circuits
git clone https://github.com/iden3/circomlib.git
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

ج قراردادهای تأیید کننده را ایجاد کنید

وارد پوشه آشکار مدار شوید.

cd reveal
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

مدار را کامپایل کنید.

circom reveal.circom --r1cs --wasm --sym
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

قرارداد groth16 مراسم و تایید کننده را ایجاد کنید.

snarkjs powersoftau new bn128 12 pot12_0000.ptau -v
snarkjs powersoftau contribute pot12_0000.ptau pot12_0001.ptau --name="First contribution" -v
snarkjs powersoftau prepare phase2 pot12_0001.ptau pot12_final.ptau -v
snarkjs groth16 setup reveal.r1cs pot12_final.ptau reveal_0000.zkey
snarkjs zkey contribute reveal_0000.zkey reveal_0001.zkey --name="1st Contributor Name" -v
snarkjs zkey export verificationkey reveal_0001.zkey verification_key.json
snarkjs zkey export solidityverifier reveal_0001.zkey ../../../contracts/src/RevealVerifier.sol
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

آن را در پوشه قرارداد MUD قرار دهید.

mkdir ../../../client/public/zk_artifacts/
cp reveal_js/reveal.wasm ../../../client/public/zk_artifacts/
cp reveal_0001.zkey ../../../client/public/zk_artifacts/reveal_final.zkey
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

حالا همین کار را با مدار دفاعی انجام دهید.

cd ../defend
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

کامپایل.

circom defend.circom --r1cs --wasm --sym
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

و تایید کننده را تولید کنید.

snarkjs powersoftau new bn128 12 pot12_0000.ptau -v
snarkjs powersoftau contribute pot12_0000.ptau pot12_0001.ptau --name="First contribution" -v
snarkjs powersoftau prepare phase2 pot12_0001.ptau pot12_final.ptau -v
snarkjs groth16 setup defend.r1cs pot12_final.ptau defend_0000.zkey
snarkjs zkey contribute defend_0000.zkey defend_0001.zkey --name="1st Contributor Name" -v
snarkjs zkey export verificationkey defend_0001.zkey verification_key.json
snarkjs zkey export solidityverifier defend_0001.zkey ../../../contracts/src/DefendVerifier.sol
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

آن را در پوشه قراردادها قرار دهید.

mkdir ../../../client/public/zk_artifacts/
cp defend_js/defend.wasm ../../../client/public/zk_artifacts/
cp defend_0001.zkey ../../../client/public/zk_artifacts/defend_final.zkey
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

شما همچنین باید نام عمومی را تغییر دهید Groth16Verifier به RevealVerifier و DefendVerifierبه ترتیب در قراردادهایی که اخیراً در آن قرار دادیم packages/client/public/zk_artifacts/.

3. منطق بازی

چند فایل را که از آنها استفاده نخواهیم کرد حذف کنید.

cd ../../../../
rm packages/contracts/src/systems/IncrementSystem.sol packages/contracts/test/CounterTest.t.sol
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

تمام منطق بازی در Solidity تعریف شده است. این شامل مراحل اولیه تخم ریزی، به دنبال آن مرحله حمله و دفاع است که هر کدام دارای اثبات ZK مربوط به خود هستند. الف را نیز اضافه کردیم killUnresponsiveCharacter عملکرد، که اگر بازیکنی نتواند در یک زمان معین اثبات ZK دفاعی خود را ارائه کند، حذف می کند.

packages/contracts/src/systems/MyGameSystem.sol

// SPDX-License-Identifier: MIT
pragma solidity >=0.8.24;

import { System } from "@latticexyz/world/src/System.sol";
import { Character, CharacterData, VerifierContracts } from "../codegen/index.sol";
import { PlayerPrivateState } from "../codegen/index.sol";
import { Direction } from "../codegen/common.sol";
import { getKeysWithValue } from "@latticexyz/world-modules/src/modules/keyswithvalue/getKeysWithValue.sol";

import { EncodedLengths, EncodedLengthsLib } from "@latticexyz/store/src/EncodedLengths.sol";

interface ICircomRevealVerifier {
    function verifyProof(uint[2] calldata _pA, uint[2][2] calldata _pB, uint[2] calldata _pC, uint[3] calldata _pubSignals) external view returns (bool);
}

interface ICircomDefendVerifier {
    function verifyProof(uint[2] calldata _pA, uint[2][2] calldata _pB, uint[2] calldata _pC, uint[4] calldata _pubSignals) external view returns (bool);
}

contract MyGameSystem is System {
  function spawn(int32 x, int32 y, uint256 commitment) public {
    //require(PlayerPrivateState.getCommitment(_msgSender()) == 0, "Player already spawned");

    Character.set(x, y, _msgSender(), 1, 0, 0, 0, false);
    Character.set(x, y + 1, _msgSender(), 2, 0, 0, 0, false);
    Character.set(x, y + 2, _msgSender(), 3, 0, 0, 0, false);
    Character.set(x, y + 3, _msgSender(), 4, 0, 0, 0, false);

    PlayerPrivateState.set(_msgSender(), commitment);
  }

  function move(int32 characterAtX, int32 characterAtY, Direction direction) public {
    CharacterData memory character = Character.get(characterAtX, characterAtY);

    //require(!character.isDead, "Character is dead");
    require(character.attackedAt == 0, "Character is under attack");
    require(character.owner == _msgSender(), "Only owner");

    int32 x = characterAtX;
    int32 y = characterAtY;

    if(direction == Direction.Up)
      y -= 1;
    if(direction == Direction.Down)
      y += 1;
    if(direction == Direction.Left)
      x -= 1;
    if(direction == Direction.Right)
      x += 1;

    CharacterData memory characterAtDestination = Character.get(x, y);
    require(characterAtDestination.owner == address(0), "Destination is occupied");

    Character.deleteRecord(characterAtX, characterAtY);
    Character.set(x, y, _msgSender(), character.id, 0, 0, character.revealedValue, false);
  }

  function attack(uint[2] calldata _pA, uint[2][2] calldata _pB, uint[2] calldata _pC, uint[3] calldata _pubSignals,
    int32 fromX, int32 fromY, int32 toX, int32 toY
  ) public {
    ICircomRevealVerifier(VerifierContracts.getRevealContractAddress()).verifyProof(_pA, _pB, _pC, _pubSignals);
    uint256 commitment = _pubSignals[0];
    uint256 characterReveal = _pubSignals[1];
    uint256 valueReveal = _pubSignals[2];

    require(PlayerPrivateState.getCommitment(_msgSender()) == commitment, "Invalid commitment");
    require(characterReveal == Character.getId(fromX, fromY), "Invalid attacker id");
    require(Character.getOwner(fromX, fromY) == _msgSender(), "You're not the planet owner");
    Character.setRevealedValue(fromX, fromY, uint32(valueReveal));
    Character.setAttackedAt(toX, toY, uint32(block.timestamp));
    Character.setAttackedByValue(toX, toY, uint32(valueReveal));
  }

  function defend(uint[2] calldata _pA, uint[2][2] calldata _pB, uint[2] calldata _pC, uint[4] calldata _pubSignals,
    int32 x, int32 y
  ) public {
    ICircomDefendVerifier(VerifierContracts.getDefendContractAddress()).verifyProof(_pA, _pB, _pC, _pubSignals);

    uint256 commitment = _pubSignals[0];
    uint256 battleResult = _pubSignals[1];
    uint256 characterTarget = _pubSignals[2];
    uint256 attackerLevel = _pubSignals[3];

    require(PlayerPrivateState.getCommitment(Character.getOwner(x, y)) == commitment, "Invalid commitment");
    require(characterTarget == Character.getId(x, y), "Invalid character id");
    require(attackerLevel == Character.getAttackedByValue(x, y), "Invalid attacked by value in proof");

    if(battleResult == 1) { // defense won
      Character.setAttackedAt(x, y, 0);
      Character.setAttackedByValue(x, y, 0);
    } else { // attack won
      Character.setIsDead(x, y, true);
    }
  }

  function killUnresponsiveCharacter(int32 x, int32 y) public {
    uint32 attackedAt = Character.getAttackedAt(x, y);
    uint32 MAX_WAIT_TIME = 1 minutes;
    require(attackedAt>0 && (attackedAt - uint32(block.timestamp)) >  MAX_WAIT_TIME, "Can kill character now");
    Character.setIsDead(x, y, true);
  }
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

به یاد داشته باشید که در MUD، ما از سازنده های سنتی استفاده نمی کنیم. این به این دلیل است که یک مجرد System قرارداد می تواند وضعیت جهان های متعدد را مدیریت کند. در عوض، ما از PostDeploy قرارداد، جایی که ما قراردادهای تأیید کننده را مستقر می کنیم.

packages/contracts/script/PostDeploy.s.sol

// SPDX-License-Identifier: MIT
pragma solidity >=0.8.24;

import { Script } from "forge-std/Script.sol";
import { console } from "forge-std/console.sol";
import { StoreSwitch } from "@latticexyz/store/src/StoreSwitch.sol";

import { RevealVerifier } from "../src/RevealVerifier.sol";
import { DefendVerifier } from "../src/DefendVerifier.sol";

import { IWorld } from "../src/codegen/world/IWorld.sol";

import { VerifierContracts } from "../src/codegen/index.sol";

contract PostDeploy is Script {
  function run(address worldAddress) external {
    // Specify a store so that you can use tables directly in PostDeploy
    StoreSwitch.setStoreAddress(worldAddress);

    // Load the private key from the `PRIVATE_KEY` environment variable (in .env)
    uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");

    // Start broadcasting transactions from the deployer account
    vm.startBroadcast(deployerPrivateKey);

    address revealVerifier = address(new RevealVerifier());
    VerifierContracts.setRevealContractAddress(revealVerifier);

    address defendVerifier = address(new DefendVerifier());
    VerifierContracts.setDefendContractAddress(defendVerifier);

    vm.stopBroadcast();
  }
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

4. مشتری با Phaser + Snarkjs

فایلی را ایجاد کنید که منطق رابط کاربری را مدیریت کند. این فایل اعمال ماوس را برای کشیدن به حرکت، اتصال دو کاراکتر به حمله و کلیک کردن برای دفاع را مدیریت می کند. علاوه بر این، منطق انیمیشن ها را تعریف می کند.

packages/client/src/layers/phaser/systems/myGameSystem.ts

import { Has, defineEnterSystem, defineExitSystem, defineSystem, getComponentValueStrict, getComponentValue } from "@latticexyz/recs";
import { PhaserLayer } from "../createPhaserLayer";
import { 
  pixelCoordToTileCoord,
  tileCoordToPixelCoord
} from "@latticexyz/phaserx";
import { TILE_WIDTH, TILE_HEIGHT, Animations, Directions } from "../constants";

function decodePosition(hexString) {
    if (hexString.startsWith('0x')) {
        hexString = hexString.slice(2);
    }

    const halfLength = hexString.length / 2;
    const firstHalfHex = hexString.slice(0, halfLength);
    const secondHalfHex = hexString.slice(halfLength);

    const firstHalfInt32 = getSignedInt32(firstHalfHex);
    const secondHalfInt32 = getSignedInt32(secondHalfHex);

    return { x: firstHalfInt32, y: secondHalfInt32 };
}

function getSignedInt32(hexStr) {
    const int32Value = parseInt(hexStr.slice(-8), 16);

    if (int32Value > 0x7FFFFFFF) {
        return int32Value - 0x100000000;
    }
    return int32Value;
}

function encodePosition(x: number, y: number): string {
    const xHex = int256ToHex(x);
    const yHex = int256ToHex(y);

    // Concatenate the two 32-byte hex values to form a 64-byte hex string
    return '0x' + xHex + yHex;
}

function int256ToHex(value: number): string {
    // If the value is negative, convert it to a 256-bit unsigned integer
    if (value  0) {
        value = BigInt('0x10000000000000000000000000000000000000000000000000000000000000000') + BigInt(value);
    } else {
        value = BigInt(value);
    }

    // Convert the integer to a hexadecimal string, ensuring it has 64 characters (256 bits)
    let hexStr = value.toString(16);
    while (hexStr.length  64) {
        hexStr = '0' + hexStr;
    }

    return hexStr;
}

export const createMyGameSystem = (layer: PhaserLayer) => {
  const {
    world,
    networkLayer: {
      components: { Character },
      systemCalls: { spawn, move, attack, defend, playerEntity }
    },
    scenes: {
        Main: { objectPool, input }
    }
  } = layer;

  let startPoint: { x: number; y: number } | null = null;
  let draggedEntity: string | null = null;

  let playerRectangle1 = objectPool.get("PlayerRectangle1", "Rectangle");
  let playerRectangle2 = objectPool.get("PlayerRectangle2", "Rectangle");
  let playerRectangle3 = objectPool.get("PlayerRectangle3", "Rectangle");
  let playerRectangle4 = objectPool.get("PlayerRectangle4", "Rectangle");
  let arrowLine1 = objectPool.get("ArrowLine1", "Line");
  let arrowLine2 = objectPool.get("ArrowLine2", "Line");
  let arrowLine3 = objectPool.get("ArrowLine3", "Line");

  let secretCharacterValues = [0, 4, 1, 2, 3];
  let privateSalt = 123;

  input.pointerdown$.subscribe((event) => {
    const { worldX, worldY } = event.pointer;
    const player = pixelCoordToTileCoord({ x: worldX, y: worldY }, TILE_WIDTH, TILE_HEIGHT);

    if (player.x === 0 && player.y === 0) return;

    let coordinates = pixelCoordToTileCoord({ x: worldX, y: worldY }, TILE_WIDTH, TILE_HEIGHT);
    let encodedPosition = encodePosition(coordinates.x, coordinates.y);

    const character = getComponentValue(Character, encodedPosition);

    if (character) {
        startPoint = { x: worldX, y: worldY };
        draggedEntity = `${player.x}-${player.y}`;
    } else {
        spawn(player.x, player.y, 123);
    }
  });

  input.pointermove$.subscribe((event) => {
    if (startPoint && draggedEntity) {
        const { worldX, worldY } = event.pointer;

        // Draw the main line
        arrowLine1.setComponent({
          id: "line",
          once: (line) => {
            line.visible = true;
            line.isStroked = true;
            line.setFillStyle(0xff00ff);
            line.geom.x1 = startPoint.x;
            line.geom.y1 = startPoint.y;
            line.geom.x2 = worldX;
            line.geom.y2 = worldY;
          },
        });

        // Draw an arrowhead effect at the end point
        const arrowLength = 20;
        const angle = Math.atan2(worldY - startPoint.y, worldX - startPoint.x);

        arrowLine2.setComponent({
          id: "line",
          once: (line) => {
            line.visible = true;
            line.isStroked = true;
            line.setFillStyle(0x00ff00);
            line.geom.x1 = worldX;
            line.geom.y1 = worldY;
            line.geom.x2 = worldX - arrowLength * Math.cos(angle - Math.PI / 6);
            line.geom.y2 = worldY - arrowLength * Math.sin(angle - Math.PI / 6);
          },
        });

        arrowLine3.setComponent({
          id: "line",
          once: (line) => {
            line.visible = true;
            line.isStroked = true;
            line.setFillStyle(0x00ff00);
            line.geom.x1 = worldX;
            line.geom.y1 = worldY;
            line.geom.x2 = worldX - arrowLength * Math.cos(angle + Math.PI / 6);
            line.geom.y2 = worldY - arrowLength * Math.sin(angle + Math.PI / 6);
          },
        });
    }
  });

  input.pointerup$.subscribe((event) => {
    if (startPoint && draggedEntity) {

      const { worldX, worldY } = event.pointer;

      const startTile = pixelCoordToTileCoord(startPoint, TILE_WIDTH, TILE_HEIGHT);
      const endTile = pixelCoordToTileCoord({ x: worldX, y: worldY }, TILE_WIDTH, TILE_HEIGHT);

      const encodedDestinationPosition = encodePosition(endTile.x, endTile.y);
      const destinationCharacter = getComponentValue(Character, encodedDestinationPosition);
      const direction = calculateDirection(startTile, endTile);

      if(startTile.x == endTile.x
          && startTile.y == endTile.y)
      {

        console.log(`Defending character at (${startTile.x}, ${startTile.y})`);

        defend(startTile.x, startTile.y,
        {
          character1: secretCharacterValues[1],
          character2: secretCharacterValues[2],
          character3: secretCharacterValues[3],
          character4: secretCharacterValues[4],
          privateSalt: privateSalt,
          characterTarget: destinationCharacter.id,
          attackerLevel: destinationCharacter.attackedByValue
        });

      } else if (destinationCharacter) {
        const encodedStartPosition = encodePosition(startTile.x, startTile.y);
        const startCharacter = getComponentValue(Character, encodedStartPosition);

        attack(startTile.x, startTile.y, endTile.x, endTile.y,
          {
            character1: secretCharacterValues[1],
            character2: secretCharacterValues[2],
            character3: secretCharacterValues[3],
            character4: secretCharacterValues[4],
            privateSalt: privateSalt,
            characterReveal: startCharacter.id,
            valueReveal: secretCharacterValues[startCharacter.id]
          }
        );
        console.log(`Attacked character from (${startTile.x}, ${startTile.y}) to (${endTile.x}, ${endTile.y})`);
      } else if (direction != null) {
        move(startTile.x, startTile.y, direction);
        console.log(`Moved character from (${startTile.x}, ${startTile.y}) to (${endTile.x}, ${endTile.y}) in direction ${direction}`);
      } 

      startPoint = null;
      draggedEntity = null;
    }
  });

  defineEnterSystem(world, [Has(Character)], ({ entity }) => {
    const character = getComponentValue(Character, entity);

    const characterObj = objectPool.get(entity, "Sprite");
    characterObj.setComponent({
      id: 'animation',
      once: (sprite) => {
        let characterAnimation = character.revealedValue;
        const playerIsOwner = "0x" + playerEntity.slice(26).toLowerCase() == "" + character.owner.toLowerCase()
        if(playerIsOwner) {
          characterAnimation = secretCharacterValues[character.id];
        }
        if(playerIsOwner) {
          const characterPosition = tileCoordToPixelCoord(decodePosition(entity), TILE_WIDTH, TILE_HEIGHT);
          let rectangle = null

          switch (character.id)  {
            case 1:
              rectangle = playerRectangle1;
              break;
            case 2:
              rectangle = playerRectangle2;
              break;
            case 3:
              rectangle = playerRectangle3;
              break;
            case 4:
              rectangle = playerRectangle4;
              break;
          }

          rectangle.setComponent({
            id: "rectangle",
            once: (rectangle) => {
              rectangle.setPosition(characterPosition.x, characterPosition.y);
              rectangle.setSize(32,32);
              rectangle.setFillStyle(0x0000ff);
              rectangle.setAlpha(0.25);
            },
          });

        }
        switch (characterAnimation)  {
          case 1:
            sprite.play(Animations.A);
            break;
          case 2:
            sprite.play(Animations.B);
            break;
          case 3:
            sprite.play(Animations.C);
            break;
          case 4:
            sprite.play(Animations.D);
            break;
          default:
            sprite.play(Animations.Unknown);
        }
      }
    });
  });

  defineExitSystem(world, [Has(Character)], ({ entity }) => {
    objectPool.remove(entity);
  });

  defineSystem(world, [Has(Character)], ({ entity }) => {
    const character = getComponentValue(Character, entity);
    if(!character)
      return;
    const pixelPosition = tileCoordToPixelCoord(decodePosition(entity), TILE_WIDTH, TILE_HEIGHT);
    const characterObj = objectPool.get(entity, "Sprite");

    if (character.isDead) {
      characterObj.setComponent({
        id: 'animation',
        once: (sprite) => {
          sprite.play(Animations.Dead);
        }
      });
    } else if (character.attackedAt != 0) {
      characterObj.setComponent({
        id: 'animation',
        once: (sprite) => {
          sprite.play(Animations.Attacked);
        }
      });
    } else
    {
      characterObj.setComponent({
        id: 'animation',
        once: (sprite) => {
          let characterAnimation = character.revealedValue;
          const playerIsOwner = "0x" + playerEntity.slice(26).toLowerCase() == "" + character.owner.toLowerCase()
          if(playerIsOwner) {
            characterAnimation = secretCharacterValues[character.id];
          }
          if(playerIsOwner) {
            //sprite.setBackgroundColor("#0000ff");
          }
          switch (characterAnimation)  {
            case 1:
              sprite.play(Animations.A);
              break;
            case 2:
              sprite.play(Animations.B);
              break;
            case 3:
              sprite.play(Animations.C);
              break;
            case 4:
              sprite.play(Animations.D);
              break;
            default:
              sprite.play(Animations.Unknown);
          }
        }
      });
    }

    characterObj.setComponent({
      id: "position",
      once: (sprite) => {
        sprite.setPosition(pixelPosition.x, pixelPosition.y);
      }
    });

    arrowLine1.setComponent({
      id: "line",
      once: (line) => {
        line.visible = false;
      },
    });

    arrowLine2.setComponent({
      id: "line",
      once: (line) => {
        line.visible = false;
      },
    });

    arrowLine3.setComponent({
      id: "line",
      once: (line) => {
        line.visible = false;
      },
    });
  });

  function calculateDirection(start: { x: number, y: number }, end: { x: number, y: number }) {
    if (end.y  start.y) return Directions.UP;
    if (end.y > start.y) return Directions.DOWN;
    if (end.x  start.x) return Directions.LEFT;
    if (end.x > start.x) return Directions.RIGHT;
    return null;
  }
};
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

5. تعامل Client-Ethereum و Client-SNARK

ابتدا باید کتابخانه ای را نصب کنیم که به ما در تولید SNARK کمک کند.

cd packages/client/
pnpm install snarkjs
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اکنون می‌توانیم تراکنش‌های زنجیره‌ای و تولید اثبات‌های ZK را تعریف کنیم.

packages/client/src/mud/createSystemCalls.ts

import { getComponentValue } from "@latticexyz/recs";
import { ClientComponents } from "./createClientComponents";
import { SetupNetworkResult } from "./setupNetwork";
import { singletonEntity } from "@latticexyz/store-sync/recs";
import { groth16 } from "snarkjs";

export type SystemCalls = ReturnTypetypeof createSystemCalls>;

export function createSystemCalls(
  { worldContract, waitForTransaction, playerEntity }: SetupNetworkResult,
  { Character }: ClientComponents,
) {
  const spawn = async (x: number, y: number) => {
    const { proof, publicSignals } = await groth16.fullProve(
      {
          character1: 4,
          character2: 1,
          character3: 2,
          character4: 3,
          privateSalt: 123,
          characterReveal: 1,
          valueReveal: 4,
      },
      "./zk_artifacts/reveal.wasm",
      "./zk_artifacts/reveal_final.zkey"
    );
    let commitment : number = publicSignals[0];
    const tx = await worldContract.write.app__spawn([x, y, commitment]);
    await waitForTransaction(tx);
    return getComponentValue(Character, singletonEntity);
  };

  const move = async (x: number, y: number, direction: number) => {
    const tx = await worldContract.write.app__move([x, y, direction]);
    await waitForTransaction(tx);
    return getComponentValue(Character,  singletonEntity);
  }

  const attack = async (fromX: number, fromY: number, toX: number, toY: number, circuitInputs: any) => {
    const { proof, publicSignals } = await groth16.fullProve(circuitInputs,
      "./zk_artifacts/reveal.wasm",
      "./zk_artifacts/reveal_final.zkey"
    );

    let pa = proof.pi_a
    let pb = proof.pi_b
    let pc = proof.pi_c
    pa.pop()
    pb.pop()
    pc.pop()

    const tx = await worldContract.write.app__attack([pa, pb, pc, publicSignals, fromX, fromY, toX, toY]);
    await waitForTransaction(tx);
    return getComponentValue(Character,  singletonEntity);
  }

  const defend = async (x: number, y: number, circuitInputs: any) => {
    const { proof, publicSignals } = await groth16.fullProve(circuitInputs,
      "./zk_artifacts/defend.wasm",
      "./zk_artifacts/defend_final.zkey"
    );

    let pa = proof.pi_a
    let pb = proof.pi_b
    let pc = proof.pi_c
    pa.pop()
    pb.pop()
    pc.pop()

    const tx = await worldContract.write.app__defend([pa, pb, pc, publicSignals, x, y]);
    await waitForTransaction(tx);
    return getComponentValue(Character,  singletonEntity);
  }

  return {
    spawn, move, attack, defend, playerEntity
  };
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

6. اضافه کردن انیمیشن های بازی

می‌توانید از هر انیمیشنی که دوست دارید استفاده کنید، اما اگر می‌خواهید از همان انیمیشن‌هایی که من استفاده می‌کنم استفاده کنید، می‌توانید این دارایی‌ها را دانلود کنید:

packages/art/sprites/A/1.png
کابالرو

packages/art/sprites/B/1.png
غول پیکر

packages/art/sprites/C/1.png
خشک

packages/art/sprites/D/1.png
اژدها

packages/art/sprites/Attacked/1.png
حمله کرد

packages/art/sprites/Dead/1.png
مرده

packages/art/sprites/Unknown/1.png
ناشناخته

اطلس را تولید کنید.

cd packages/art
yarn
yarn generate-multiatlas-sprites
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

و انیمیشن های بازی را تعریف می کنیم. در اینجا می توانید انیمیشن های چند فریمی اضافه کنید و رفتار و سرعت آنها را تنظیم کنید.

انیمیشن های بازی را تعریف کنید. می توانید شهرت، سرعت و رفتار را تنظیم کنید.

packages/client/src/layers/phaser/configurePhaser.ts

import Phaser from "phaser";
import {
  defineSceneConfig,
  AssetType,
  defineScaleConfig,
  defineMapConfig,
  defineCameraConfig,
} from "@latticexyz/phaserx";
import worldTileset from "../../../public/assets/tilesets/world.png";
import { TileAnimations, Tileset } from "../../artTypes/world";
import { Assets, Maps, Scenes, TILE_HEIGHT, TILE_WIDTH, Animations } from "./constants";

const ANIMATION_INTERVAL = 200;

const mainMap = defineMapConfig({
  chunkSize: TILE_WIDTH * 64, // tile size * tile amount
  tileWidth: TILE_WIDTH,
  tileHeight: TILE_HEIGHT,
  backgroundTile: [Tileset.Grass],
  animationInterval: ANIMATION_INTERVAL,
  tileAnimations: TileAnimations,
  layers: {
    layers: {
      Background: { tilesets: ["Default"] },
      Foreground: { tilesets: ["Default"] },
    },
    defaultLayer: "Background",
  },
});

export const phaserConfig = {
  sceneConfig: {
    [Scenes.Main]: defineSceneConfig({
      assets: {
        [Assets.Tileset]: {
          type: AssetType.Image,
          key: Assets.Tileset,
          path: worldTileset,
        },
        [Assets.MainAtlas]: {
          type: AssetType.MultiAtlas,
          key: Assets.MainAtlas,
          // Add a timestamp to the end of the path to prevent caching
          path: `/assets/atlases/atlas.json?timestamp=${Date.now()}`,
          options: {
            imagePath: "/assets/atlases/",
          },
        },
      },
      maps: {
        [Maps.Main]: mainMap,
      },
      sprites: {
      },
      animations: [
        {
          key: Animations.A,
          assetKey: Assets.MainAtlas,
          startFrame: 1,
          endFrame: 1,
          frameRate: 3,
          repeat: -1,
          duration: 1,
          prefix: "sprites/A/",
          suffix: ".png",
        },
        {
          key: Animations.B,
          assetKey: Assets.MainAtlas,
          startFrame: 1,
          endFrame: 1,
          frameRate: 3,
          repeat: -1,
          duration: 1,
          prefix: "sprites/B/",
          suffix: ".png",
        },
        {
          key: Animations.C,
          assetKey: Assets.MainAtlas,
          startFrame: 1,
          endFrame: 1,
          frameRate: 3,
          repeat: -1,
          duration: 1,
          prefix: "sprites/C/",
          suffix: ".png",
        },
        {
          key: Animations.D,
          assetKey: Assets.MainAtlas,
          startFrame: 1,
          endFrame: 1,
          frameRate: 3,
          repeat: -1,
          duration: 1,
          prefix: "sprites/D/",
          suffix: ".png",
        },
        {
          key: Animations.Dead,
          assetKey: Assets.MainAtlas,
          startFrame: 1,
          endFrame: 1,
          frameRate: 12,
          repeat: -1,
          duration: 1,
          prefix: "sprites/Dead/",
          suffix: ".png",
        },
        {
          key: Animations.Unknown,
          assetKey: Assets.MainAtlas,
          startFrame: 1,
          endFrame: 1,
          frameRate: 12,
          repeat: -1,
          duration: 1,
          prefix: "sprites/Unknown/",
          suffix: ".png",
        },
        {
          key: Animations.Attacked,
          assetKey: Assets.MainAtlas,
          startFrame: 1,
          endFrame: 1,
          frameRate: 12,
          repeat: -1,
          duration: 1,
          prefix: "sprites/Attacked/",
          suffix: ".png",
        },
      ],
      tilesets: {
        Default: {
          assetKey: Assets.Tileset,
          tileWidth: TILE_WIDTH,
          tileHeight: TILE_HEIGHT,
        },
      },
    }),
  },
  scale: defineScaleConfig({
    parent: "phaser-game",
    zoom: 1,
    mode: Phaser.Scale.NONE,
  }),
  cameraConfig: defineCameraConfig({
    pinchSpeed: 1,
    wheelSpeed: 1,
    maxZoom: 3,
    minZoom: 1,
  }),
  cullingChunkSize: TILE_HEIGHT * 16,
};
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

7. همه چیز را به هم متصل کنید

packages/client/src/layers/phaser/constants.ts

export enum Scenes {
  Main = "Main",
}

export enum Maps {
  Main = "Main",
}

export enum Animations {
  A = "A",
  B = "B",
  C = "C",
  D = "D",
  Dead = "Dead",
  Unknown = "Unknown",
  Attacked = "Attacked",
}

export enum Directions {
  UP = 0,  
  DOWN = 1,  
  LEFT = 2,  
  RIGHT = 3,  
}

export enum Assets {
  MainAtlas = "MainAtlas",
  Tileset = "Tileset",
}

export const TILE_HEIGHT = 32;
export const TILE_WIDTH = 32;
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

packages/client/src/layers/phaser/systems/registerSystems.ts

import { PhaserLayer } from "../createPhaserLayer";
import { createCamera } from "./createCamera";
import { createMapSystem } from "./createMapSystem";
import { createMyGameSystem } from "./myGameSystem";

export const registerSystems = (layer: PhaserLayer) => {
  createCamera(layer);
  createMapSystem(layer);
  createMyGameSystem(layer);
};
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

8. بازی را اجرا کنید

به فهرست اصلی پروژه برگردید.

cd ../../
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

و بازی را اجرا کنید.

pnpm dev
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اکنون می توانید بازی را در دو مرورگر مختلف باز کنید. هر بازیکن می تواند با کلیک بر روی یک فضای خالی تخم ریزی کند. برای حرکت به فضای خالی مجاور بکشید. برای حمله به بازیکن دیگری بکشید. برای ایجاد SNARK دفاعی روی بازیکن مورد حمله کلیک کنید.

نسخه ی نمایشی بازی

در بازی برای تخم ریزی کلیک کنید، برای حرکت به فضای خالی مجاور بکشید، برای حمله به سمت حریف بکشید و برای دفاع کلیک کنید.

با تشکر از خواندن این راهنما!

من را در dev.to و YouTube برای همه چیزهایی که به توسعه بلاک چین به زبان اسپانیایی مرتبط است دنبال کنید.

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا