Zum Hauptinhalt springen
Echtzeit-Radar & Feeds
Alle RSS Feeds ➔
👥 Community & Social
Windows Tipps & SecurityGrafikkarte vor Überhitzung schützen: So geht’s(25.09.2026 um 08:00 Uhr)
••••••••••
Windows Tipps & SecurityGrafikkarte vor Überhitzung schützen: So geht’s(25.09.2026 um 08:00 Uhr)
••••••••••
Intelligence View
⚡ tsecurity.de Intelligence

Big Handmade Maps 100% On-Chain? Is it Possible?

Dark Forest has shown that procedurally generated maps can be both engaging to the players and cost-effective in terms of on-chain gas usage. However, in their procgen article, Nalin and Gubsheep (AW developers) expressed that creating…

0
↗ Quelle (dev.to)
Reagiere als Erste:r — dein Feedback zählt!

Dark Forest has shown that procedurally generated maps can be both engaging to the players and cost-effective in terms of on-chain gas usage. However, in their procgen article, Nalin and Gubsheep (AW developers) expressed that creating handmade maps on-chain presents significant challenges. Inspired by this, I decided to take on the challenge of finding a scalable way to store large handmade maps on-chain.



In this tutorial, we will go into both the theory and practice of creating fully on-chain handcrafted maps. By creating a game where the map has obstacles (mountains) stored in a Merkle Tree optimized for data compression. The player will have to submit Merkle Inclusion Proofs in order to advance.



merkle tree game demo



Prefer to see the complete code? Head to Github to find all the code mentioned in this guide.






Starting with the Theory






Naive Implementation: Avoid this 🙅



Let's begin with a simple map where 0 represents grass and 1 represents mountains. The player can walk only on grass.



Map Bidimensional



Our first intuition might be to declare a two-dimensional mapping and populate it like this:




mapping(uint x => mapping(uint y => terrainType)) map;
map[1][0] = 1;
map[2][2] = 1;
map[2][3] = 1;
map[0][3] = 1;






However, this approach is impractical for large maps due to the high gas costs and block size limitations. To store larger maps on-chain efficiently, we need a more scalable approach: Merkle Trees.






Merkleizing a Map



In this tutorial, we’ll transform a two-dimensional map into a Merkle tree. Players will prove their position by submitting a Merkle inclusion proof for the type of terrain they are on.






Why Merkleize a Map?



Merkleizing a map allows for proofs in logarithmic time, as opposed to linear. However, before Merkleizing, we must convert the map into a one-dimensional array using the following formula:






LEAF_INDEX=y×MAP_WIDTH+x
\text{LEAF{\textunderscore}INDEX} = y \times \text{MAP{\textunderscore}WIDTH} + x
LEAF_INDEX=y×MAP_WIDTH+x




For example, our 3x3 map would convert into the following array:



Bidimensional Map in unidimensional array



Merkleizing a one-dimensional array is more straightforward than doing so for a two-dimensional map. The process involves hashing adjacent elements (positions 0 and 1, 2 and 3, etc.) until we reach the Merkle root.



Merkleized map



When the game launches, the only data posted on-chain is the Merkle root. As players move, they submit Merkle proofs to verify their moves. This approach spreads the cost of storing the map across all players rather than placing the entire burden on the deployer.






In practice: Creating a big map on MUD



Supporting Material: How to Create and Autonomous World Game



Let's start by creating a new MUD project.




If you haven't installed MUD collapse this and install the dependencies.




curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash nvm install 20
curl -L https://foundry.paradigm.xyz | bash export PATH=$PATH:~/.foundry/bin
sudo npm install -g pnpm









Once you're ready create a phaser template.




pnpm create mud@latest tutorial --template phaser
cd tutorial









The data



For this demo we will test a 32x32 map that we will define on the following public file.



packages/client/public/assets/map.json




{
"map": [
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1],
[0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0],
[0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
]
}









The table



Our table will consist of the players position, where every address can control only one player. Also a Singleton that holds the map merkle root as a commitment so no one can cheat later in the game.



packages/contracts/mud.config.ts




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

export default defineWorld({
namespace: "app",
enums: {
Direction: [
"Up",
"Down",
"Left",
"Right"
]
},
tables: {
PlayerPosition: {
schema: {
player: "address",
x: "uint32",
y: "uint32",
},
key: ["player"]
},
Map: {
schema: {
merkleRoot: "bytes32",
size: "uint32"
},
key: [],
},
},
});









The contract



First let's remove the normal files.




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






Now let's commit to a merkle root in our PostDeploy script. Keep in mind that if you change the map data it will result in a new merkle root. I conveniently print the root on the terminal so you can grab it from there.



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 { IWorld } from "../src/codegen/world/IWorld.sol";

import { Map } 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);

// Initialize Map
Map.set(
0xc99004d76733dbd8a4a6f3f3ecdc08392637d31e4339cce7c2b2aa7220e85fbf,
32
);

vm.stopBroadcast();
}
}






And define the movement logic on chain. Notice we verify the merkle inclusion proofs on chain to check if the player is walking into grass and not a mountain.



packages/contracts/src/systems/MyGameSystem.sol




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

import { System } from "@latticexyz/world/src/System.sol";
import { PlayerPosition, PlayerPositionData, Map } 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";

contract MyGameSystem is System {
function spawn(uint32 x, uint32 y) public {
address player = _msgSender();
PlayerPosition.set(player, x, y);
}

function move(Direction direction, bytes32 positionLeaf, bytes32[] calldata proof) public {
require(positionLeaf == bytes32(0), "Must move to walkable area"); // 0 is grass, 1 is mountains
address player = _msgSender();
PlayerPositionData memory playerPosition = PlayerPosition.get(player);

uint32 x = playerPosition.x;
uint32 y = playerPosition.y;

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

PlayerPosition.set(player, x, y);

require(
verify(
positionLeaf,
Map.getMerkleRoot(),
proof,
getLeafIndex(x, y)),
"invalid proof"
);
}

function verify(
bytes32 leaf,
bytes32 root,
bytes32[] calldata proof,
uint256 leafIndex
) internal pure returns (bool) {
bytes32 computedHash = leaf;
for (uint256 i = 0; i < proof.length; i++) {
if (leafIndex % 2 == 0) {
computedHash = keccak256(abi.encodePacked(computedHash, proof[i]));
} else {
computedHash = keccak256(abi.encodePacked(proof[i], computedHash));
}
leafIndex /= 2;
}
return computedHash == root;
}

function getLeafIndex(uint32 x, uint32 y) public view returns (uint32) {
// Calculate the leaf index as y * mapWidth + x
return y * Map.getSize() + x;
}
}









The client






The map interpreter



Let's modify the default Map System to interpret our map.json file.



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




import { Tileset } from "../../../artTypes/world";
import { PhaserLayer } from "../createPhaserLayer";

export async function createMapSystem(layer: PhaserLayer) {
const {
scenes: {
Main: {
maps: {
Main: { putTileAt },
},
},
},
} = layer;

try {
const response = await fetch('/assets/map.json');
const data = await response.json();
const map: number[][] = data.map;

for (let y = 0; y < map.length; y++) {
for (let x = 0; x < map[y].length; x++) {
const coord = { x: x, y: y };
const tileType = map[y][x];

if (tileType === 1) {
putTileAt(coord, Tileset.Mountain, "Foreground");
} else {
putTileAt(coord, Tileset.Grass, "Background");
}
}
}
} catch (error) {
console.error("Error loading the map:", error);
}
}









The Client: User interaction and Merkle Proofs generation



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




import { Has, defineEnterSystem, defineSystem, getComponentValueStrict } from "@latticexyz/recs";
import { PhaserLayer } from "../createPhaserLayer";
import {
pixelCoordToTileCoord,
tileCoordToPixelCoord
} from "@latticexyz/phaserx";
import { TILE_WIDTH, TILE_HEIGHT, Animations, Directions } from "../constants";
import { keccak256, toUtf8Bytes, zeroPadValue } from 'ethers';

// Utils
function hashFunction(data: Uint8Array): string {
return keccak256(data);
}

function hexStringToBytes(hex: string): Uint8Array {
if (hex.startsWith('0x')) {
hex = hex.slice(2);
}
const bytes = new Uint8Array(hex.length / 2);
for (let i = 0; i < hex.length; i += 2) {
bytes[i / 2] = parseInt(hex.substr(i, 2), 16);
}
return bytes;
}

// Build the Merkle Tree
function buildMerkleTree(leafNodes: (number | string)[]): string {
// Convert leaf nodes to bytes32 if they are numbers
const processedLeafNodes = leafNodes.map(node =>
typeof node === 'number' ? zeroPadValue("0x0"+node, 32) : node
);

let level = processedLeafNodes;
while (level.length > 1) {
const nextLevel: string[] = [];
for (let i = 0; i < level.length; i += 2) {
const left = level[i];
const right = i + 1 < level.length ? level[i + 1] : left;
const combined = new Uint8Array([
...hexStringToBytes(left),
...hexStringToBytes(right)
]);
nextLevel.push(hashFunction(combined));
}
level = nextLevel;
}
return level[0];
}

// Hash the entire map
function hashMap(map: number[][]): string {
const flatMap: (number | string)[] = map.flat();
return buildMerkleTree(flatMap);
}

interface HashPath {
leafHash: string;
path: string[]; // Just hashes in the path
}

// Generate the hash path for a specific position
function generateHashPath(map: number[][], x: number, y: number): HashPath {
const flatMap: (number | string)[] = map.flat();
const index = y * map[0].length + x;
const leafHash = typeof flatMap[index] === 'number'
? zeroPadValue("0x0"+flatMap[index], 32)
: flatMap[index];
const path: string[] = [];
let level = flatMap.map(value => typeof value === 'number' ? zeroPadValue("0x0"+value, 32) : value);

let currentIndex = index;

while (level.length > 1) {
const nextLevel: string[] = [];
const levelLength = level.length;

for (let i = 0; i < levelLength; i += 2) {
const left = level[i];
const right = i + 1 < levelLength ? level[i + 1] : left;
const combined = new Uint8Array([
...hexStringToBytes(left),
...hexStringToBytes(right)
]);
const parentHash = hashFunction(combined);
nextLevel.push(parentHash);

if (i === currentIndex || i + 1 === currentIndex) {
const siblingIndex = i === currentIndex ? i + 1 : i;
path.push(level[siblingIndex]); // Store only the hash
currentIndex = Math.floor(currentIndex / 2); // Move up to the parent index
}
}
level = nextLevel;
}

return { leafHash, path };
}

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

let myPosition = {x: 0, y: 0};
let map: number[][];

const loadMap = async () => {
try {
const response = await fetch('/assets/map.json');
const data = await response.json();
map = data.map;
console.log("Map loaded");

const mapHash = hashMap(map);
console.log('Map Hash (Merkle Root):', mapHash);

} catch (error) {
console.error("Error loading the map:", error);
}
};

loadMap();

input.pointerdown$.subscribe((event) => {
const x = event.pointer.worldX;
const y = event.pointer.worldY;
const playerPosition = pixelCoordToTileCoord({ x, y }, TILE_WIDTH, TILE_HEIGHT);
console.log(playerPosition)
if(playerPosition.x == 0 && playerPosition.y == 0)
return;
spawn(playerPosition.x, playerPosition.y)
});

input.onKeyPress((keys) => keys.has("W"), () => {
myPosition.y -= 1;
console.log(myPosition)

const path = generateHashPath(map, myPosition.x, myPosition.y);

console.log('Leaf Hash:', path.leafHash);
console.log('Leaf Index:', myPosition.y * map[0].length + myPosition.x);

const proof = path.path.map(hash => `0x${hash.slice(2)}`);
console.log('Proof:', JSON.stringify(proof));

move(Directions.UP, path.leafHash, proof);
});

input.onKeyPress((keys) => keys.has("S"), () => {
myPosition.y += 1;
console.log(myPosition)

const path = generateHashPath(map, myPosition.x, myPosition.y);

console.log('Leaf Hash:', path.leafHash);
console.log('Leaf Index:', myPosition.y * map[0].length + myPosition.x);

const proof = path.path.map(hash => `0x${hash.slice(2)}`);
console.log('Proof:', JSON.stringify(proof));

move(Directions.DOWN, path.leafHash, proof);
});

input.onKeyPress((keys) => keys.has("A"), () => {
myPosition.x -= 1;
console.log(myPosition)

const path = generateHashPath(map, myPosition.x, myPosition.y);

console.log('Leaf Hash:', path.leafHash);
console.log('Leaf Index:', myPosition.y * map[0].length + myPosition.x);

const proof = path.path.map(hash => `0x${hash.slice(2)}`);
console.log('Proof:', JSON.stringify(proof));

move(Directions.LEFT, path.leafHash, proof);
});

input.onKeyPress((keys) => keys.has("D"), () => {
myPosition.x += 1;
console.log(myPosition)

const path = generateHashPath(map, myPosition.x, myPosition.y);

console.log('Leaf Hash:', path.leafHash);
console.log('Leaf Index:', myPosition.y * map[0].length + myPosition.x);

const proof = path.path.map(hash => `0x${hash.slice(2)}`);
console.log('Proof:', JSON.stringify(proof));

move(Directions.RIGHT, path.leafHash, proof);
});

defineEnterSystem(world, [Has(PlayerPosition)], ({entity}) => {
const playerObj = objectPool.get(entity, "Sprite");
playerObj.setComponent({
id: 'animation',
once: (sprite) => {
sprite.play(Animations.Player);
}
})
});

defineSystem(world, [Has(PlayerPosition)], ({ entity }) => {
const playerPosition = getComponentValueStrict(PlayerPosition, entity);
myPosition = playerPosition;
const pixelPosition = tileCoordToPixelCoord(playerPosition, TILE_WIDTH, TILE_HEIGHT);

const playerObj = objectPool.get(entity, "Sprite");

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









The Animations, System Calls and Registration



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";

export type SystemCalls = ReturnType<typeof createSystemCalls>;

export function createSystemCalls(
{ worldContract, waitForTransaction }: SetupNetworkResult,
{ PlayerPosition }: ClientComponents,
) {
const spawn = async (x: number, y: number) => {
const tx = await worldContract.write.app__spawn([x, y]);
await waitForTransaction(tx);
return getComponentValue(PlayerPosition, singletonEntity);
};

const move = async (direction: number, leaf: string, proof: string[]) => {
const tx = await worldContract.write.app__move([direction, leaf, proof]);
await waitForTransaction(tx);
return getComponentValue(PlayerPosition, singletonEntity);
};

return {
spawn, move
};
}






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 { Sprites, 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: {
[Sprites.Player]: {
assetKey: Assets.MainAtlas,
frame: "sprites/golem/idle/0.png",
},
},
animations: [
{
key: Animations.Player,
assetKey: Assets.MainAtlas,
startFrame: 0,
endFrame: 3,
frameRate: 6,
repeat: -1,
prefix: "sprites/golem/idle/",
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,
};






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




export enum Scenes {
Main = "Main",
}

export enum Maps {
Main = "Main",
}

export enum Animations {
Player = "Player",
}
export enum Sprites {
Player,
}

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);
};






For simplicity, on this demo we set a fixed camera where the 0,0 position is the top left position.



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




import { createPhaserEngine } from "@latticexyz/phaserx";
import { namespaceWorld } from "@latticexyz/recs";
import { NetworkLayer } from "../network/createNetworkLayer";
import { registerSystems } from "./systems";

export type PhaserLayer = Awaited<ReturnType<typeof createPhaserLayer>>;
type PhaserEngineConfig = Parameters<typeof createPhaserEngine>[0];

export const createPhaserLayer = async (networkLayer: NetworkLayer, phaserConfig: PhaserEngineConfig) => {
const world = namespaceWorld(networkLayer.world, "phaser");

const { game, scenes, dispose: disposePhaser } = await createPhaserEngine(phaserConfig);
world.registerDisposer(disposePhaser);

const { camera } = scenes.Main;

camera.phaserCamera.setBounds(0, 0, 500, 500);
camera.phaserCamera.centerOn(0, 0);

const components = {};

const layer = {
networkLayer,
world,
game,
scenes,
components,
};

registerSystems(layer);

return layer;
};









Run the game



Install the ethers dependency.




cd packages/client/
pnpm install ethers
cd ../..






Run the game.




pnpm dev









Closing thoughts






Encoding expressive worlds



Instead of a 0 1 data. We could encode data into each position with no gas usage increase. For example we could encode the following structure:




struct MapTile {
uint8 terrainType;
uint8 fishApparationRate;
uint8 wildCatApparationRate;
bool isExplored;
string npcDialog;
[...]
}









Only submit proofs once



To save gas and offchain indexing, players should submit a merkle proof once. All proof should be stored so players in the future can query the map that has been already explored on chain.



For simplicity I'm not doing it on this guide but it should look something like this:




mapping(uint x => mapping(uint y => bytes32 terrainData)) mapData;
mapping(uint x => mapping(uint y => bool isExplored)) mapIsExplored;

function move(Direction direction, bytes32 positionLeaf, bytes32[] calldata proof) public {
[...]
mapData[x][y] = positionLeaf;
mapIsExplored[x][y] = true;
}

function move(Direction direction) public {
[...]
require(mapIsExplored[x][y], "Tile not explored yet");
}









Bigger maps require Web2 optimizations



Maybe a specialized indexer backend that precalculates the whole map merkle inclusion proofs so players can consult. In terms of gas cost this demo scales quite well do it's logarithmic verification costs. For example I did the following test:




  • Moving in a 32x32 map costs 92,687 gas.

  • Moving a 1000x1000 map cost 103,440 gas.



gas comparation



As you can see, the map size doesn't impact too much on the gas cost. The rest of the logic on the move function is more significant.



For this test I used this map whose merkle root computes to 0x86f3820289c9335418aaa077ba6a1dc6ab512203cc1faecb450bfbfe64021e98.



Thanks for reading this guide!



Follow Filosofía Código on dev.to and in Youtube for everything related to Blockchain development.

1. Sofort-Triage & Abwehrmaßnahmen

SOC Incident Playbook: Vulnerability Remediation & Verification
Syntax validiert (0 Fehler)
title: Detect Exploitation - Big Handmade Maps 100% On-Chain? Is it Possible?
id: 6802e886-e662-4a67-9014-c8cae6345adf
status: experimental
description: Automatisch generierte SIEM-Erkennungsregel basierend auf CTI Intelligence
references:
  - https://tsecurity.de/
author: iShareStuff CTI Automated Detection Engine
date: 2026-09-26
logsource:
  category: network_connection
  product: any
detection:
  selection:
      DestinationHostname:
        - 'foundry.paradigm.xyz'
        - 'dev.to'
  condition: selection
falsepositives:
  - Legitime administrative Zugriffe oder Penetrationstests
level: high
tags:
  - attack.initial_access
Syntax validiert (0 Fehler)
rule CTI_Threat_Indicator {
    meta:
        author = "iShareStuff CTI Automated Detection Engine"
        date = "2026-09-26"
        description = "YARA Signature for "
    strings:
        $str = "Big Handmade Maps 100% On-Chai" ascii wide
    condition:
        any of them
}
Syntax validiert (0 Fehler)
index=security sourcetype IN ("cisco:asa", "pan:traffic", "zeek_conn", "suricata", "WinEventLog:Security")
(dest_host="foundry.paradigm.xyz" OR dest_host="dev.to")
| stats count earliest(_time) as first_seen latest(_time) as last_seen by src_ip, dest_ip, dest_host, signature
| eval first_seen=strftime(first_seen, "%Y-%m-%d %H:%M:%S"), last_seen=strftime(last_seen, "%Y-%m-%d %H:%M:%S")
| sort - count
Syntax validiert (0 Fehler)
destination.domain: ("foundry.paradigm.xyz" OR "dev.to") and event.category: "network"
Syntax validiert (0 Fehler)
CommonSecurityLog
| where DestinationHostName in ("foundry.paradigm.xyz", "dev.to")
| summarize EventCount = count(), FirstSeen = min(TimeGenerated), LastSeen = max(TimeGenerated) by SourceIP, DestinationIP, DestinationPort, Activity
| extend DetectionRule = "iShareStuff-CTI-Compiled"
| sort by EventCount desc

2. Cyber Threat Intelligence & Forensik

IoC Intelligence (2 Indikatoren)
foundry[.]paradigm[.]xyzdev[.]to
CTI Threat Relationship Graph3 Knoten / 2 Relationen
CVE / Incident Software MITRE ATT&CK CWE Weakness IoC
🎯
MITRE ATT&CK Matrix Navigator 14 Taktiken
Reconnaissance
-
Resource Development
-
Initial Access
Execution
Persistence
-
Privilege Escalation
Defense Evasion
Credential Access
-
Discovery
-
Lateral Movement
-
Collection
-
Command and Control
Exfiltration
-
Impact
tsecurity.de Cognitive Threat RAG
Fokus-Vektor:

Kognitive Analyse für identifizierte Bedrohung: Erhöhte Bedrohungslage im Bereich Big Handmade Maps 100% On-Chain? Is it P.... Basierend auf 368k Vektor-Korrelationen werden sofortige Isolationsmaßnahmen für betroffene Endpunkte empfohlen.

🛡️ Angriffsfläche & Exposure

Netzwerk/Remote-Zugriff ohne Vorauthentifizierung möglich.

⚡ Empfohlene Sofortmaßnahmen
  • 1. Perimeter-Inspektion: Relevante Portfreigaben und exponierte Endpunkte unverzüglich scannen.
  • 2. Patch-Applikation: Hersteller-Hotfix einspielen oder betroffene Daemons in isolierte DMZ-Segmente überführen.
  • 3. Telemetrie & EDR-Alerts: Prozessaufrufe und Child-Processes auf anomale Shell-Spawns überwachen.
🔗 Semantisch verwandte Zero-Days MariaDB 11.7 VEC
Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten Big Handmade Maps 100% On-Chain? Is it Possible?

Thematisch verwandte Begriffe: Handmade, Maps, OnChain, Possible · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Zum Aktualisieren ziehen
ZERO-DAY CVE-2026-100539 | OpenClaw (npm package 'openclaw') before 2026.8.1 fails to revoke memor…
Advisory →
tsecurity.de Icon
Offline-Lesen, Eilmeldungen & 0ms Ladezeit

Installiere tsecurity.de direkt auf deinen Home-Bildschirm für das ultimative Vollbild-Magazinerlebnis ohne Browser-Leisten.

Nächster Beitrag