-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Robin Bryce
committed
Nov 12, 2024
1 parent
e2b06ee
commit 2d660ae
Showing
16 changed files
with
894 additions
and
20 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 @@ | ||
CONTRACTS_CHAINTRAP_DIR=../../contracts/chaintrap |
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 |
---|---|---|
@@ -1,2 +1,4 @@ | ||
dist | ||
dungeon | ||
abi.json | ||
.env.secrets* |
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,12 @@ | ||
version: '3' | ||
|
||
dotenv: | ||
- ".env" | ||
- ".env.secret" | ||
|
||
tasks: | ||
abi: | ||
desc: "output the abi" | ||
cmds: | ||
- | | ||
task -d $CONTRACTS_CHAINTRAP_DIR abi |
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
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,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); | ||
} | ||
} | ||
} |
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,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; | ||
} |
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
Oops, something went wrong.