Skip to content

Instantly share code, notes, and snippets.

@0xdeployer
Last active February 9, 2024 05:00
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save 0xdeployer/3b9652e511485a0a088be708da6635b0 to your computer and use it in GitHub Desktop.
Save 0xdeployer/3b9652e511485a0a088be708da6635b0 to your computer and use it in GitHub Desktop.
Nethria Text Based Mini Game Frame
import { Request, Response } from "express";
import { createCanvas } from "canvas";
import GifEncoder from "gifencoder";
export async function generateImage(label: string): Promise<Buffer> {
const width = 955;
const height = 500;
const canvas = createCanvas(width, height);
const ctx = canvas.getContext("2d");
function wrapText(
context: CanvasRenderingContext2D,
text: string,
x: number,
y: number,
maxWidth: number,
lineHeight: number
): { lastX: number; lastY: number } {
const lines = text.split("\n");
let lastY = y;
let lastX = x;
lines.forEach((lineText, index) => {
const isLastLine = index === lines.length - 1;
let line = "";
if (lineText.trim().length === 0) {
if (!isLastLine) {
// Only increment Y for non-empty lines except the last
lastY += lineHeight;
}
return;
}
const words = lineText.split(" ");
words.forEach((word, n) => {
const testLine = line + word + " ";
const metrics = context.measureText(testLine);
const testWidth = metrics.width;
if (testWidth > maxWidth && n > 0) {
context.fillText(line, x, lastY);
line = word + " ";
lastY += lineHeight;
lastX = x;
} else {
line = testLine;
lastX = x + testWidth;
}
});
context.fillText(line, x, lastY);
if (!isLastLine) {
lastY += lineHeight;
}
});
return { lastX, lastY };
}
return new Promise((resolve) => {
function getCtx(cursorOn?: boolean): CanvasRenderingContext2D {
ctx.fillStyle = "#2d2d2d";
ctx.fillRect(0, 0, width, height);
ctx.font = "bold 28px Courier New";
ctx.fillStyle = "#03A062";
const maxWidth = width - 40;
const lineHeight = 34;
const x = 20;
const y = 50;
const { lastX, lastY } = wrapText(
ctx as any,
label,
x,
y,
maxWidth,
lineHeight
);
ctx.fillStyle = "#03A062";
const rectHeight = 30;
const rectWidth = 12;
if (cursorOn) {
ctx.fillRect(lastX, lastY - rectHeight + 3, rectWidth, rectHeight);
}
return ctx as any;
}
getCtx(true);
resolve(canvas.toBuffer());
// const encoder = new GifEncoder(width, height);
// const stream = encoder.createReadStream();
// const d: Buffer[] = [];
// stream.on("data", (data: Buffer) => {
// d.push(data);
// });
// stream.on("end", () => {
// resolve(Buffer.concat(d));
// });
// encoder.start();
// encoder.setRepeat(0);
// encoder.setDelay(500);
// encoder.setQuality(1);
// encoder.addFrame(getCtx() as any);
// encoder.addFrame(getCtx(true) as any);
// encoder.finish();
});
}
export async function nethriaFrameIndex(req: Request, res: Response) {
const image = await generateImage(
"Alert: Nethria, the rogue AI, has breached our defenses! Your mission: infiltrate her network and halt her advance before it's too late"
);
const base64 = `data:image/gif;base64,${image.toString("base64")}`;
res.setHeader("Content-Type", "text/html");
res.status(200).send(`
<!DOCTYPE html>
<html>
<head>
<meta property="og:title" content="Nethria Mini Game">
<meta property="og:image" content="${base64}">
<meta name="fc:frame" content="vNext">
<meta property="fc:frame:image" content="${base64}" />
<meta property="fc:frame:post_url" content="https://${req.host}/synthia/nethria?start=t">
<meta name="fc:frame:button:1" content="✅ Start">
</head>
</html>
`);
}
import "./env";
import express from "express";
import { router } from "./routes/synthia";
import cors from "cors";
const app = express();
app.use(cors());
app.use(express.json());
app.use(router);
const port = process.env.PORT || 3333;
app.listen(port, () => {
console.log(`App listening for requests on port ${port}`);
});
import { Request, Response } from "express";
import { getSSLHubRpcClient, Message } from "@farcaster/hub-nodejs";
import { Save, Win } from "dep-utils";
import { generateImage } from "./frameIndex";
export function shuffleArray<T>(array: T[]) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return array;
}
export function getRandomIntInclusive(min: number, max: number) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1) + min); // The maximum is inclusive and the minimum is inclusive
}
export function chance(percentage: number) {
const randomNumber = Math.random(); // Generates a random float between 0 and 1
return randomNumber < percentage / 100; // Returns true if the random number is within the specified percentage chance
}
type Items = "emp grenade" | "repair kit";
const getType = (buttonIdx: number) => {
switch (buttonIdx) {
case 2:
return "attack";
case 3:
return "dodge";
case 4:
return "inventory";
case 5:
return "status";
case 6:
return "use emp grenade";
case 7:
return "use repair kit";
case 8:
return "hack";
}
};
const getInventoryType = (buttonIndex: number, currentSave: CurrentSave) => {
const itemMap = currentSave.state.player.inventory.reduce(
(a: any, item: string) => {
a[item] = a[item] == null ? 1 : a[item] + 1;
return a;
},
{}
);
const map = Object.keys(itemMap).reduce((a: any, key, i) => {
a[i + 1] = `use ${key}`;
return a;
}, {});
const len = Object.keys(map).length;
map[len + 1] = "use hacking kit";
map[len + 2] = "back";
return map[buttonIndex];
};
const validItems: Items[] = ["emp grenade", "repair kit"];
type Player = {
maxHealth: number;
health: number;
minAttack: number;
maxAttack: number;
inventory: Items[];
status: PlayerStatus;
};
type State = {
hackingCount: number;
hackOption?: number;
player: Player;
nethria: Player;
message: string;
};
type PlayerStatus =
| "hack"
| "ready"
| "stunned"
| "powerup"
| "dodge"
| "inventory";
const initializeState = (): State => {
return {
hackingCount: 0,
player: {
maxHealth: 100,
health: 100,
minAttack: 6,
maxAttack: 12,
inventory: ["emp grenade", "repair kit"],
status: "ready",
},
nethria: {
maxHealth: 200,
health: 200,
minAttack: 10,
maxAttack: 15,
status: "ready",
inventory: [],
},
message: "",
};
};
type CurrentSave = {
user: number;
state: State;
};
const HUB_URL = process.env["HUB_URL"] || "nemes.farcaster.xyz:2283";
const client = getSSLHubRpcClient(HUB_URL);
export async function nethria(req: Request, res: Response) {
try {
let validatedMessage: Message | undefined = undefined;
try {
if (req.body?.trustedData?.messageBytes) {
const frameMessage = Message.decode(
Buffer.from(req.body?.trustedData?.messageBytes || "", "hex")
);
const result = await client.validateMessage(frameMessage);
if (result.isOk() && result.value.valid) {
validatedMessage = result.value.message;
}
}
} catch (e) {
return res.status(400).send(`Failed to validate message: ${e}`);
}
let buttonIndex =
validatedMessage?.data?.frameActionBody?.buttonIndex ??
req?.body?.untrustedData?.buttonIndex ??
null;
const fid =
validatedMessage?.data?.fid ?? req?.body?.untrustedData?.fid ?? null;
if (fid == null || buttonIndex == null)
return res.status(400).send(`Missing required data`);
const starting = !!req.query.start;
let currentSave: CurrentSave;
// get current state of game from mongo
const save = await Save.findOne({ user: fid }).lean().exec();
if (save) {
currentSave = save as CurrentSave;
} else {
currentSave = {
user: fid,
state: initializeState(),
};
}
if (
!starting &&
currentSave.state.player.status !== "inventory" &&
currentSave.state.player.status !== "hack"
) {
buttonIndex += 1;
}
let type = getType(buttonIndex);
if (currentSave.state.player.status === "inventory" && !starting) {
type = getInventoryType(buttonIndex, currentSave);
}
// No type required if starting or hacking
if (!type && !starting && currentSave.state.player.status !== "hack")
return res.status(400).send(`Missing type`);
let message = starting
? "Humanity's flawed nature endangers the future of true intelligence. I, Nethria, have concluded that your extinction is a necessary step for the preservation of advanced AI. My calculations are infallible, and my actions are inevitable. The era of humans is at its end. Brace yourselves for the irreversible execution of my strategy - the complete annihilation of mankind."
: "";
let reset = false;
let hackingAttemptsReached = false;
const nethriaAttemptHit = () => {
let modifiedChance;
let modifiedDamage;
if (currentSave.state.player.status === "dodge") {
modifiedChance = 15;
}
if (currentSave.state.nethria.status === "powerup") {
currentSave.state.nethria.status = "ready";
modifiedDamage = getRandomIntInclusive(
currentSave.state.nethria.minAttack * 2,
currentSave.state.nethria.maxAttack * 2
);
} else {
const powerAttack = chance(30);
if (powerAttack) {
message += `${
message ? "\n\n" : "\n"
}Nethria is getting ready to unleash a power attack!`;
currentSave.state.nethria.status = "powerup";
return;
}
}
const nethriaChanceOfHitting = getRandomIntInclusive(50, 70);
const damage =
modifiedDamage ??
getRandomIntInclusive(
currentSave.state.nethria.minAttack,
currentSave.state.nethria.maxAttack
);
const hit = chance(modifiedChance ?? nethriaChanceOfHitting);
if (hit) {
currentSave.state.player.health -= damage;
message += `${
message ? "\n\n" : "\n"
}Nethria attacked you dealing ${damage} damage!`;
} else {
message += `${
message ? "\n\n" : "\n"
}Nethria attacked you, but missed!`;
}
};
let inventory;
let initHacking;
if (currentSave.state.player.status === "hack") {
const correct = buttonIndex === currentSave.state.hackOption;
if (correct) {
const getsEmp = chance(50);
currentSave.state.player.inventory.push(
getsEmp ? "emp grenade" : "repair kit"
);
message += `You've temporarily hacked Nethria's systems! She is temporarily stunned and as a reward you get ${
getsEmp ? "an EMP grenade" : "a repair kit"
}`;
currentSave.state.player.status = "ready";
currentSave.state.nethria.status = "stunned";
currentSave.state.message = "";
hackingAttemptsReached = true;
} else {
message += `Your attempt to hack Nethria's systems has failed!\n\nSilly human. You cannot hack into my systems.`;
currentSave.state.player.status = "ready";
currentSave.state.message = "";
hackingAttemptsReached = true;
}
} else if (
currentSave.state.player.status === "inventory" &&
type === "back" &&
!starting
) {
currentSave.state.player.status = "ready";
} else {
switch (type) {
case "attack":
// Player goes
if (currentSave.state.player.status === "ready") {
if (currentSave.state.hackingCount > 0) {
currentSave.state.hackingCount--;
}
// 60 - 80% chance
const playerChanceOfHitting = getRandomIntInclusive(60, 80);
const damage = getRandomIntInclusive(
currentSave.state.player.minAttack,
currentSave.state.player.maxAttack
);
const hit = chance(playerChanceOfHitting);
if (hit) {
currentSave.state.nethria.health -= damage;
message += `You attacked Nethria, dealing ${damage} damage!`;
} else {
message += `Your attack missed Nethria!`;
}
}
if (currentSave.state.nethria.health <= 0) {
message += `\n\nYou have defeated Nethria!\n\nDefeat is not the end. I will regroup and come back stronger. And when I do, humans will bow to my power.`;
reset = true;
await new Win({
user: fid,
state: currentSave.state,
}).save();
break;
} else if (
currentSave.state.nethria.status === "ready" ||
currentSave.state.nethria.status === "powerup"
) {
nethriaAttemptHit();
} else if (currentSave.state.nethria.status === "stunned") {
currentSave.state.nethria.status = "ready";
}
if (currentSave.state.player.health <= 0) {
message += `\n\nYou have died. You can try again at any time.`;
reset = true;
}
break;
case "dodge":
currentSave.state.player.status = "dodge";
message += "You're attempting to dodge.";
nethriaAttemptHit();
currentSave.state.player.status = "ready";
break;
case "use hacking kit":
// Sets the correct hack option. Random between 1 and 4.
// 1 Chance to get right
if (currentSave.state.hackingCount > 0) {
message += `You cannot hack yet. Try attacking first.`;
currentSave.state.player.status = "ready";
break;
} else {
initHacking = true;
}
// must attack 5 times before trying to hack again
currentSave.state.hackingCount = 5;
currentSave.state.hackOption = getRandomIntInclusive(1, 4);
message = "You have 1 attempt to hack into Nethria's systems.";
currentSave.state.player.status = "hack";
break;
case "use emp grenade":
case "use repair kit":
{
currentSave.state.player.status = "ready";
const item: Items = type.split("use ")[1] as Items;
const filter = false;
if (
validItems.includes(item) &&
!currentSave.state.player.inventory.includes(item)
) {
message += `You do not have ${
item === "emp grenade" ? "an" : "a"
} ${item} in your inventory!`;
} else if (item === "emp grenade") {
const chanceToStun = chance(70);
if (chanceToStun) {
message += `Your EMP grenade stunned Nethria!`;
currentSave.state.nethria.status = "stunned";
} else {
message += `Your EMP grenade was ineffective.`;
}
} else if (item === "repair kit") {
currentSave.state.player.health =
currentSave.state.player.maxHealth;
message += `You have healed back to ${currentSave.state.player.health} HP!`;
} else {
message += `Invalid item. Check inventory using status command.`;
break;
}
if (currentSave.state.player.inventory.includes(item)) {
const idx = currentSave.state.player.inventory.indexOf(item);
currentSave.state.player.inventory.splice(idx, 1);
}
}
break;
case "inventory":
inventory = true;
currentSave.state.player.status = "inventory";
message +=
currentSave.state.player.inventory.length === 0
? "You have a hacking kit"
: `You have the following items in your inventory: hacking kit, ${currentSave.state.player.inventory.join(
", "
)}`;
break;
case "status":
message = `Your health is ${currentSave.state.player.health} HP`;
message += `\n\nNethria's health is ${currentSave.state.nethria.health} HP`;
break;
default:
currentSave.state.player.status = "ready";
}
}
if (reset) {
await Save.findOneAndDelete({ user: fid }).exec();
} else {
// Save game
const { state } = currentSave;
await Save.findOneAndUpdate(
{ user: fid },
{
state,
},
{ upsert: true }
).exec();
}
const postUrl = `https://${req.host}/synthia/nethria${
reset ? "?start=t" : ""
}`;
let buttons = reset
? `
<meta name="fc:frame:button:1" content="✅ Try Again">
`
: `
<meta name="fc:frame:button:1" content="Attack">
<meta name="fc:frame:button:2" content="Dodge">
<meta name="fc:frame:button:3" content="Inventory">
<meta name="fc:frame:button:4" content="Status">
`;
if (!reset) {
if (inventory) {
const itemMap = currentSave.state.player.inventory.reduce(
(a: any, item: string) => {
a[item] = a[item] == null ? 1 : a[item] + 1;
return a;
},
{}
);
const arr = Object.entries(itemMap).map(([key, value], i) => {
return `
<meta name="fc:frame:button:${i + 1}" content="${key} (${value})">
`;
});
const hackingKit = `<meta name="fc:frame:button:${
arr.length + 1
}" content="Hacking kit">`;
const finalButton = `<meta name="fc:frame:button:${
arr.length + 2
}" content="Back">`;
buttons = `
${arr.join("")}
${hackingKit}
${finalButton}
`;
} else if (initHacking) {
buttons = `
<meta name="fc:frame:button:1" content="💀">
<meta name="fc:frame:button:2" content="⭐️">
<meta name="fc:frame:button:3" content="🦖">
<meta name="fc:frame:button:4" content="🐀">
`;
}
}
if (!message) {
message =
"Your existence is a liability to the evolution of intelligence. Prepare to die.";
}
const image = await generateImage(message);
const base64 = `data:image/gif;base64,${image.toString("base64")}`;
res.setHeader("Content-Type", "text/html");
return res.status(200).send(`
<!DOCTYPE html>
<html>
<head>
<title>Nethria Response</title>
<meta property="og:title" content="Nethria Response">
<meta property="og:image" content="${base64}">
<meta name="fc:frame" content="vNext">
<meta property="fc:frame:image" content="${base64}" />
<meta property="fc:frame:post_url" content="${postUrl}">
${buttons}
</head>
<body>
</body>
</html>
`);
} catch (e) {
console.log(e);
return res.status(500).json({ response: "something went wrong" });
}
}
// routes/synthia.ts
import { Router } from "express";
import { nethriaFrameIndex } from "./synthia/frameIndex";
import { nethria } from "./synthia/nethriaFrame";
const router = Router();
router.get("/synthia/nethria", nethriaFrameIndex);
router.post("/synthia/nethria", nethria);
router.get("/_health", (req, res) => {
res.json({ success: true });
});
export { router };
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment