-
Notifications
You must be signed in to change notification settings - Fork 19
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
1 parent
cda5dd5
commit 2d6dd9a
Showing
9 changed files
with
717 additions
and
22 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); |
Oops, something went wrong.