Skip to content

Commit

Permalink
feat: chaintrap: transactor
Browse files Browse the repository at this point in the history
  • Loading branch information
Robin Bryce committed Nov 12, 2024
1 parent e2b06ee commit 2d660ae
Show file tree
Hide file tree
Showing 16 changed files with 894 additions and 20 deletions.
1 change: 1 addition & 0 deletions apps/cartog/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
CONTRACTS_CHAINTRAP_DIR=../../contracts/chaintrap
2 changes: 2 additions & 0 deletions apps/cartog/.gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
dist
dungeon
abi.json
.env.secrets*
12 changes: 12 additions & 0 deletions apps/cartog/Taskfile.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
version: '3'

dotenv:
- ".env"
- ".env.secret"

tasks:
abi:
desc: "output the abi"
cmds:
- |
task -d $CONTRACTS_CHAINTRAP_DIR abi
4 changes: 3 additions & 1 deletion apps/cartog/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
"author": "",
"license": "MIT",
"devDependencies": {
"@polysensus-dapper/svelte-onepagedungeon": "workspace:^"
"@polysensus-dapper/svelte-onepagedungeon": "workspace:^",
"@polysensus-dapper/chaintrap": "workspace:^"
},
"dependencies": {
"@types/jsdom": "^21.1.7",
Expand All @@ -23,6 +24,7 @@
"jsdom": "^25.0.0",
"ts-node": "^10.9.1",
"typescript": "^5.6.2",
"viem": "^2.21.44",
"xmldom": "^0.6.0"
}
}
155 changes: 155 additions & 0 deletions apps/cartog/src/creategame.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import type { Command } from "commander";
import { InvalidArgumentError, Option } from "commander";
import { privateKeyToAccount } from 'viem/accounts';
import {
createPublicClient, createWalletClient, parseEventLogs, http, toHex } from 'viem';
import type { TransactionReceipt, Log } from "viem";
import { redstone } from 'viem/chains'

import {
parseDungeon, MerkleCodec, ObjectTypes,
} from "@polysensus-dapper/svelte-onepagedungeon"

import { readJson } from "./fsutil.js";

import { createTopology } from "./createtopology.js";
import { DungeonOptions, dungeonName, resolveSeed, loadJsonDugeon } from "./jsondungeon.js";

import { Transactor } from "./transactor/transactor.js";
import { RequestorOptions } from "./transactor/options.js";
import { TransactResult } from "./transactor/result.js";
import { ParsedLog } from "./transactor/parsedlog.js";

type CreateGameOptions = {
url: string
abi: string
contract: `0x${string}`
narratorKey: `0x${string}`
};
type Options = DungeonOptions & CreateGameOptions;

function parseBase10(value: string): number {
const parsed = parseInt(value, 10);
if (isNaN(parsed))
throw new InvalidArgumentError("Not a number");
return parsed;
}

export function addCreateGameCommand(program: Command): void {
program
.command("creategame")
.description("create a chaintrap game from a watabou dungeon")
.addOption(new Option("-u, --url <url>", "The URL for the chain").default("http://127.0.0.1:8545").env("RPC_URL"))
.addOption(new Option("-C, --contract <contract>", "The address of the contract").env("CHAINTRAP_ADDRESS"))
.addOption(new Option("--narrator-key <narratorkey>", "Private Key").env("NARATOR_KEY"))
.option("--abi <abi>", "The ABI file to use", "abi.json")
.option("-f, --file <file>", "The JSON file to analyze")
.option("-S, --seeds <seeds>", "A file with seedes for each known title")
.option("-s, --seed <seed>", "An explicit seed for the random number generator", parseBase10)
.option("-o, --output <output>", "The output directory", "./dungeon/<name>/tiles")
.action(async (options: Options) => {
await createGame(program, options);
});
}

const out = console.log;
let vout = (...args: any[]) => { };

function buildTree(options: Options) {
const watabouJson = loadJsonDugeon(options.file);
const name = dungeonName(watabouJson);
const outputDir = options.output.replace("<name>", name);
const seed = resolveSeed(options, watabouJson);
const dungeon = parseDungeon(seed, watabouJson);
const s = JSON.stringify(dungeon, null, ' ')
const topo = createTopology(dungeon, name);
return topo.commit();
}

async function methodCaller(method: string, ...args: any): Promise<TransactionReceipt> {
/*
const { request } = await reader.simulateContract({
account: account,
address: options.contract,
abi: abi,
functionName: method,
args: args,
});
const hash = await signer.writeContract(request);
return await reader.getTransactionReceipt({ hash });
*/
return {} as TransactionReceipt;
}


async function createGame(program: Command, options: Options) {

out(`@${options.contract}:createGame() ${options.file}`);

if (!options.output)
throw new Error("Output directory not specified");
const tree = buildTree(options);

const abi = readJson(options.abi).abi;
const account = privateKeyToAccount(options.narratorKey);
const reader = createPublicClient({
chain: redstone,
transport: http(options.url),
});
const signer = createWalletClient({
account,
chain: redstone,
transport: http(options.url),
});

const requestorOptions: RequestorOptions<TransactionReceipt> = {
//logParser: (receipt) => ({ receipt, names: [], events: {} }),
logParser: (receipt) => {
const logs = parseEventLogs({abi, logs: (receipt as TransactionReceipt).logs});
const parsed: ParsedLog[] = [];
for (const log of logs) {
const log2 = log as unknown as ParsedLog;
parsed.push({...log2, signature: log.topics[0] ?? ""});
}
return parsed;
},
methodCaller: async <T>(method: string, ...args: any): Promise<T> => {
const { request } = await reader.simulateContract({
account: account,
address: options.contract,
abi: abi,
functionName: method,
args: args,
});
const hash = await signer.writeContract(request);
const receipt = await reader.getTransactionReceipt({ hash });
return receipt as T;
}
}

const initArgs = {
tokenURI: "https://chaintrap.polysensus.io/dungeon/{id}",
registrationLimit: 2,
trialistArgs: {flags: 0, lives:2},
rootLabels:[toHex("chaintrap-dungeon:static".padStart(32, '0'))],
roots: [tree.root],
choiceInputTypes: [ObjectTypes.E.location_choices].map(MerkleCodec.conditionInput),
transitionTypes: [ObjectTypes.E.link, ObjectTypes.E.finish].map(MerkleCodec.conditionInput),
victoryTransitionTypes: [ObjectTypes.E.finish].map(MerkleCodec.conditionInput),
haltParticipantTransitionTypes: [ObjectTypes.E.fatal_chest_trap].map(MerkleCodec.conditionInput),
livesIncrement: [ObjectTypes.E.chest_treat_gain_life].map(MerkleCodec.conditionInput),
livesDecrement: [ObjectTypes.E.chest_treat_gain_life].map(MerkleCodec.conditionInput),
}


const transactor = new Transactor<TransactionReceipt>(requestorOptions)
.method('createGame', initArgs)
.requireLogs('TranscriptCreated', 'TranscriptMerkleRootSet');


for await (const r of transactor.transact()) {
for (const log of r.ordered) {
out(log.eventName);
}
}
}
178 changes: 178 additions & 0 deletions apps/cartog/src/createtopology.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import {
facingDirection as watFacingDirection,
LogicalTopology,
Furnishing as CTFurnishing,
// Furniture as CTFurniture,
Furniture,
} from "@polysensus-dapper/svelte-onepagedungeon"
import type { Dungeon } from "@polysensus-dapper/svelte-onepagedungeon";

// import type { JSONConnection, Dungeon, ObjectType, Point, Door } from "@polysensus-dapper/svelte-onepagedungeon";
import type {
JSONConnection as CTJSONConnection,
JSONLocationItem as CTJSONLocationItem,
JSONLocation as CTJSONLocation,
} from "@polysensus-dapper/svelte-onepagedungeon";

import type {
Door as WatDoor,
Room as WatRoom,
Exit as WatExit,
} from "@polysensus-dapper/svelte-onepagedungeon";

export function createTopology(dungeon: Dungeon, name: string = "A Chaintrap Dungeon"): LogicalTopology {

const opposite = (dir: string): string => {
const opp = { "north": "south", "east": "west", "south": "north", "west": "east" }[dir];
if (!opp) throw new Error(`Invalid direction ${dir}`);
return opp;
}

// chaintrap direction codes are anti-clockwise, watabou are clockwise
const ctdirnum = (dir: string): number => {
const n = { "north": 0, "west": 1, "south": 2, "east": 3 }[dir];
if (typeof n === 'undefined') throw new Error(`Invalid direction ${dir}`);
return n;
}
const ctnumopposit = (dir: number): number => {
const n = { 0: 2, 1: 3, 2: 0, 3: 1 }[dir];
if (typeof n === 'undefined') throw new Error(`Invalid direction ${dir}`);
return n;
}

// watabou direction codes are clockwise, chaintrap are anti-clockwise
const watdirnum = (dir: string): number => {
const n = { "north": 0, "east": 1, "south": 2, "west": 3 }[dir];
if (typeof n === 'undefined') throw new Error(`Invalid direction ${dir}`);
return n;
}

const connections: Record<number, CTJSONConnection & WatDoor & { id: number }> =
dungeon.doors.reduce((acc: Record<number, CTJSONConnection & WatDoor & { id: number }>, door: WatDoor & { id: number }) => {
const facing = watFacingDirection(door)
const conn: CTJSONConnection & WatDoor & { id: number } = {
id: door.id,
type: door.type,
dir: door.dir,
x: door.x,
y: door.y,
joins: [-1, -1],
join_sides: [-1, -1],
points: [[door.x, door.y]],
};
acc[door.id] = conn;
return acc;
}, {});

const watFinish: CTJSONLocation & WatRoom = {
id: dungeon.rooms.length,
description: "Finish",
area: "1m x 1m",
notes: [],
exits: [],
corridors: [[], [], [], []],
main: false,
inter: false,
x: 0, y: 0, w: 0, h: 0, l: 0
}


// The relationship bewtween the watabou format and the chaintrap format is esesntially 1:1 for things that matter.
// * Watabou room or area-rect has at most 4 exits - chain trap has arbitrary exits per side.
// * Watabou sorts exits clockwise from the top - chaintrap sorts anti-clockwise.
// * Watabou doors corresponds to a chaintrap connection/corridor
// * Watabou doors (connections) are unit squares
// * Watabou doors (connections) join only opposite sides, chaintrap joins allow for right angled corridor bends.
const locations: Record<number, CTJSONLocation & WatRoom> = dungeon.rooms.reduce((acc: Record<number, CTJSONLocation & WatRoom>, room: WatRoom) => {
const loc: CTJSONLocation & WatRoom = {
id: room.id,
description: room.description,
comment: room.description,
area: room.area,
main: true,
inter: false,
notes: room.notes,
exits: room.exits,
corridors: [[], [], [], []],
x: room.x,
y: room.y,
w: room.w,
l: room.h,
h: room.h,
};
room.exits.forEach((exit: WatExit) => {
const iexit = ctdirnum(exit.towards)
// watabou models connections as 1x1 locations, each of which has an id in
// the same space as the rooms. the connections are all numerically
// larger than legit locations.
loc.corridors[iexit].push((typeof exit.to === 'string') ? watFinish.id : exit.to);

const ijoin = exit.isFacing ? 0 : 1;

connections[exit.door.id].joins[ijoin] = room.id;

// the join side is the opposite of the exit side
connections[exit.door.id].join_sides[ijoin] = ctdirnum(opposite(exit.towards));

return loc;
})
acc[room.id] = loc;
return acc;
}, {});

// satisfies some merkle encoding requirements
//locations[watFinish.id] = watFinish;

// find the finish location
const finish = Object.values(locations).filter((loc) => {
// exits that lead outside are marked as via connection -1
// watabou supports many, the merkleization currently only supports one
for (const exit of loc.exits)
if (exit.to === 'outside') return true;
return false;
})[0];
const finishExit = finish.exits.filter((exit) => exit.to === 'outside')[0];
const finishSide = ctdirnum(finishExit.towards);

// clean up the entrances
let dangling = [];
for (const [id, conn] of Object.entries(connections)) {
if (conn.joins[0] === -1 || conn.joins[1] === -1) {
dangling.push(id);
/*;
if (conn.joins[0] === -1) {
if (conn.joins[1] === -1) throw new Error("null connection");
conn.joins = [watFinish.id, conn.joins[1]];
conn.join_sides[0] = ctnumopposit(conn.join_sides[1]);
watFinish.corridors[conn.join_sides[0]].push(conn.id);
}
if (conn.joins[1] === -1) {
if (conn.joins[0] === -1) throw new Error("null connection");
conn.joins = [conn.joins[1], watFinish.id];
conn.join_sides[1] = ctnumopposit(conn.join_sides[0]);
watFinish.corridors[conn.join_sides[1]].push(conn.id);
}
*/}
}

// remove the corridor connection (watabou has one connection per side)
// finish.corridors[finishSide] = [];

const finishItem: CTJSONLocationItem = {
unique_name: "finish_exit",
labels: ["victory_condition"],
type: "finish_exit",
choiceType: "finish_exit",
data: { location: finish.id, side: finishSide, exit: 0 },
meta: { notes: [finishExit.description] },
}
if (typeof finishExit.note === 'string')
finishItem.meta.notes.push(finishExit.note);

const topo = new LogicalTopology();
topo.extendJoins(Object.values(connections).sort((a, b) => a.id - b.id));
topo.extendLocations(Object.values(locations).sort((a, b) => a.id - b.id));
topo.placeFinish(new CTFurnishing(0, finishItem));
topo.placeFurniture(new Furniture({ map: { name, beta: "" }, items: [finishItem] }));
return topo;
}
2 changes: 2 additions & 0 deletions apps/cartog/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ import { program } from "commander";

import { addExplodeCommand } from "./explodesvg.js";
import { addMerklizeCommand } from "./merkleize.js";
import { addCreateGameCommand } from "./creategame.js";

program
.enablePositionalOptions()
.option("-v, --verbose", "more verbose reporting")

addExplodeCommand(program);
addMerklizeCommand(program);
addCreateGameCommand(program);

try {
program.parse();
Expand Down
Loading

0 comments on commit 2d660ae

Please sign in to comment.