Skip to content

Commit

Permalink
feat: add grid collision example
Browse files Browse the repository at this point in the history
Add a new example with grid 2d where collision is implemented.
This utilizes the `resolveConflicts` CRO function for conflict resolution
on the user position.

This also reveals an issue with the `this` context within the `object`
package when `resolveConflicts` is callback from `TopologyObject`.
  • Loading branch information
cwkang1998 committed Nov 7, 2024
1 parent cda5dd5 commit 2d6dd9a
Show file tree
Hide file tree
Showing 9 changed files with 717 additions and 22 deletions.
18 changes: 18 additions & 0 deletions examples/grid-collision/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Topology Protocol Collision Example

This is an example that uses Topology Protocol to implement a 2D grid space where users appear to be circles and can move around the integer grid one grid at a time. We additionally implement collision logic into this example so that no 2 circles can be on one grid at a time.

## Specifics

The Grid CRO has a mapping from user id (node id concacenated with a randomly assigned color string) to the user's position on the grid. The CRO leverages the underlying hash graph for conflict-free consistency. The mergeCallback function receives the linearised operations returned from the underlying hash graph, and recomputes the user-position mapping from those operations.

The `resolveConflict` function is additionally used to implement compensation techniques in order to ensure no 2 node can be on the same circle at a time.

## How to run locally

After cloning the repository, run the following commands:

```bash
cd ts-topology/examples/grid-collision
pnpm dev
```
42 changes: 42 additions & 0 deletions examples/grid-collision/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Topology - Grid</title>
</head>
<body>
<div>
<h1>A 2D grid made with CRO</h1>
<p>Your Peer ID: <span id="peerId"></span></p>
<p>Peers on dRAM: <span id="peers"></span></p>
<p>Discovery Peers: <span id="discoveryPeers"></span></p>

<button id="createGrid">Spawn a new Grid CRO</button>
<span style="margin: 0 10px;">|</span>
<input id="gridInput" type="text" placeholder="Enter Grid CRO ID" />
<button id="joinGrid">Connect to existing Grid CRO</button>
<p>
Connected to Grid CRO ID:
<span id="gridId" style="text-decoration: underline;"></span>
<button id="copyGridId" style="margin-left: 10px; display: none;">Copy</button>
</p>
<p>Peers in CRO: <span id="objectPeers"></span></p>
</div>

<div
id="grid"
style="
position: relative;
width: 100%;
height: 60vh;
border: 1px solid black;
overflow: hidden;
"
>
<!-- Users will appear here -->
</div>

<script type="module" src="/src/index.ts"></script>
</body>
</html>
35 changes: 35 additions & 0 deletions examples/grid-collision/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"name": "topology-example-grid-collision",
"version": "0.2.1-0",
"description": "Topology Protocol Grid Example",
"main": "src/index.ts",
"repository": "https://github.com/topology-foundation/ts-topology.git",
"license": "MIT",
"scripts": {
"build": "vite build",
"clean": "rm -rf dist/ node_modules/",
"dev": "vite serve",
"start": "ts-node ./src/index.ts"
},
"dependencies": {
"@topology-foundation/network": "0.3.0",
"@topology-foundation/node": "0.3.0",
"@topology-foundation/object": "0.3.0",
"assemblyscript": "^0.27.29",
"crypto-browserify": "^3.12.0",
"memfs": "^4.11.1",
"process": "^0.11.10",
"react-spring": "^9.7.4",
"stream-browserify": "^3.0.0",
"ts-node": "^10.9.2",
"uint8arrays": "^5.1.0",
"vm-browserify": "^1.1.2"
},
"devDependencies": {
"@types/node": "^22.5.4",
"ts-loader": "^9.5.1",
"typescript": "^5.5.4",
"vite": "^5.4.9",
"vite-plugin-node-polyfills": "^0.22.0"
}
}
276 changes: 276 additions & 0 deletions examples/grid-collision/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
import { TopologyNode } from "@topology-foundation/node";
import type { TopologyObject } from "@topology-foundation/object";
import { Grid } from "./objects/grid";
import { hslToRgb, rgbToHex, rgbToHsl } from "./util/color";

const node = new TopologyNode({
network_config: {
bootstrap_peers: [
"/dns4/relay.droak.sh/tcp/443/wss/p2p/Qma3GsJmB47xYuyahPZPSadh1avvxfyYQwk8R3UnFrQ6aP",
"/ip4/0.0.0.0/tcp/50000/ws/p2p/12D3KooWC6sm9iwmYbeQJCJipKTRghmABNz1wnpJANvSMabvecwJ",
],
},
});
let topologyObject: TopologyObject;
let gridCRO: Grid;
let peers: string[] = [];
let discoveryPeers: string[] = [];
let objectPeers: string[] = [];

const formatNodeId = (id: string): string => {
return `${id.slice(0, 4)}...${id.slice(-4)}`;
};

const colorMap: Map<string, string> = new Map();

const hashCode = (str: string): number => {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = (hash << 5) - hash + str.charCodeAt(i);
hash |= 0; // Convert to 32bit integer
}
return hash;
};

const getColorForNodeId = (id: string): string => {
if (!colorMap.has(id)) {
const hash = hashCode(id);
let r = (hash & 0xff0000) >> 16;
let g = (hash & 0x00ff00) >> 8;
let b = hash & 0x0000ff;

// Convert to HSL and adjust lightness to be below 50%
let [h, s, l] = rgbToHsl(r, g, b);
l = l * 0.5; // Set lightness to below 50%

// Convert back to RGB
[r, g, b] = hslToRgb(h, s, l);
const color = rgbToHex(r, g, b); // Convert RGB to hex
colorMap.set(id, color);
}
return colorMap.get(id) || "#000000";
};

const render = () => {
if (topologyObject) {
const gridIdElement = <HTMLSpanElement>document.getElementById("gridId");
gridIdElement.innerText = topologyObject.id;
const copyGridIdButton = document.getElementById("copyGridId");
if (copyGridIdButton) {
copyGridIdButton.style.display = "inline"; // Show the button
}
} else {
const copyGridIdButton = document.getElementById("copyGridId");
if (copyGridIdButton) {
copyGridIdButton.style.display = "none"; // Hide the button
}
}

const element_peerId = <HTMLDivElement>document.getElementById("peerId");
element_peerId.innerHTML = `<strong style="color: ${getColorForNodeId(node.networkNode.peerId)};">${formatNodeId(node.networkNode.peerId)}</strong>`;

const element_peers = <HTMLDivElement>document.getElementById("peers");
element_peers.innerHTML = `[${peers.map((peer) => `<strong style="color: ${getColorForNodeId(peer)};">${formatNodeId(peer)}</strong>`).join(", ")}]`;

const element_discoveryPeers = <HTMLDivElement>(
document.getElementById("discoveryPeers")
);
element_discoveryPeers.innerHTML = `[${discoveryPeers.map((peer) => `<strong style="color: ${getColorForNodeId(peer)};">${formatNodeId(peer)}</strong>`).join(", ")}]`;

const element_objectPeers = <HTMLDivElement>(
document.getElementById("objectPeers")
);
element_objectPeers.innerHTML = `[${objectPeers.map((peer) => `<strong style="color: ${getColorForNodeId(peer)};">${formatNodeId(peer)}</strong>`).join(", ")}]`;

if (!gridCRO) return;
const users = gridCRO.getUsers();
const element_grid = <HTMLDivElement>document.getElementById("grid");
element_grid.innerHTML = "";

const gridWidth = element_grid.clientWidth;
const gridHeight = element_grid.clientHeight;
const centerX = Math.floor(gridWidth / 2);
const centerY = Math.floor(gridHeight / 2);

// Draw grid lines
const numLinesX = Math.floor(gridWidth / 50);
const numLinesY = Math.floor(gridHeight / 50);

for (let i = -numLinesX; i <= numLinesX; i++) {
const line = document.createElement("div");
line.style.position = "absolute";
line.style.left = `${centerX + i * 50}px`;
line.style.top = "0";
line.style.width = "1px";
line.style.height = "100%";
line.style.backgroundColor = "lightgray";
element_grid.appendChild(line);
}

for (let i = -numLinesY; i <= numLinesY; i++) {
const line = document.createElement("div");
line.style.position = "absolute";
line.style.left = "0";
line.style.top = `${centerY + i * 50}px`;
line.style.width = "100%";
line.style.height = "1px";
line.style.backgroundColor = "lightgray";
element_grid.appendChild(line);
}

for (const userColorString of users) {
const [id, color] = userColorString.split(":");
const position = gridCRO.getUserPosition(userColorString);

if (position) {
const div = document.createElement("div");
div.style.position = "absolute";
div.style.left = `${centerX + position.x * 50 + 5}px`; // Center the circle
div.style.top = `${centerY - position.y * 50 + 5}px`; // Center the circle
if (id === node.networkNode.peerId) {
div.style.width = `${34}px`;
div.style.height = `${34}px`;
} else {
div.style.width = `${34 + 6}px`;
div.style.height = `${34 + 6}px`;
}
div.style.backgroundColor = color;
div.style.borderRadius = "50%";
div.style.transition = "background-color 1s ease-in-out";
div.style.animation = `glow-${id} 0.5s infinite alternate`;

// Add black border for the current user's circle
if (id === node.networkNode.peerId) {
div.style.border = "3px solid black";
}

// Create dynamic keyframes for the glow effect
const style = document.createElement("style");
style.innerHTML = `
@keyframes glow-${id} {
0% {
background-color: ${hexToRgba(color, 0.5)};
}
100% {
background-color: ${hexToRgba(color, 1)};
}
}`;
document.head.appendChild(style);

element_grid.appendChild(div);
}
}
};

// Helper function to convert hex color to rgba
function hexToRgba(hex: string, alpha: number) {
const bigint = Number.parseInt(hex.slice(1), 16);
const r = (bigint >> 16) & 255;
const g = (bigint >> 8) & 255;
const b = bigint & 255;
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
}

async function addUser() {
if (!gridCRO) {
console.error("Grid CRO not initialized");
alert("Please create or join a grid first");
return;
}

gridCRO.addUser(
node.networkNode.peerId,
getColorForNodeId(node.networkNode.peerId),
);
render();
}

async function moveUser(direction: string) {
if (!gridCRO) {
console.error("Grid CRO not initialized");
alert("Please create or join a grid first");
return;
}

gridCRO.moveUser(node.networkNode.peerId, direction);
render();
}

async function createConnectHandlers() {
node.addCustomGroupMessageHandler(topologyObject.id, (e) => {
if (topologyObject)
objectPeers = node.networkNode.getGroupPeers(topologyObject.id);
render();
});

node.objectStore.subscribe(topologyObject.id, (_, obj) => {
render();
});
}

async function main() {
await node.start();
render();

node.addCustomGroupMessageHandler("", (e) => {
peers = node.networkNode.getAllPeers();
discoveryPeers = node.networkNode.getGroupPeers("topology::discovery");
render();
});

const button_create = <HTMLButtonElement>(
document.getElementById("createGrid")
);
button_create.addEventListener("click", async () => {
topologyObject = await node.createObject(new Grid());
gridCRO = topologyObject.cro as Grid;
createConnectHandlers();
await addUser();
render();
});

const button_connect = <HTMLButtonElement>document.getElementById("joinGrid");
button_connect.addEventListener("click", async () => {
const croId = (<HTMLInputElement>document.getElementById("gridInput"))
.value;
try {
topologyObject = await node.createObject(
new Grid(),
croId,
undefined,
true,
);
gridCRO = topologyObject.cro as Grid;
createConnectHandlers();
await addUser();
render();
console.log("Succeeded in connecting with CRO", croId);
} catch (e) {
console.error("Error while connecting with CRO", croId, e);
}
});

document.addEventListener("keydown", (event) => {
if (event.key === "w") moveUser("U");
if (event.key === "a") moveUser("L");
if (event.key === "s") moveUser("D");
if (event.key === "d") moveUser("R");
});

const copyButton = <HTMLButtonElement>document.getElementById("copyGridId");
copyButton.addEventListener("click", () => {
const gridIdText = (<HTMLSpanElement>document.getElementById("gridId"))
.innerText;
navigator.clipboard
.writeText(gridIdText)
.then(() => {
// alert("Grid CRO ID copied to clipboard!");
console.log("Grid CRO ID copied to clipboard");
})
.catch((err) => {
console.error("Failed to copy: ", err);
});
});
}

main();
Loading

0 comments on commit 2d6dd9a

Please sign in to comment.