mirror of
https://github.com/Dvorinka/SEEN.git
synced 2026-06-03 20:13:02 +00:00
small fix, don't worry about it
This commit is contained in:
@@ -0,0 +1,356 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { chromium } from "playwright";
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = {
|
||||
url: null,
|
||||
iterations: 3,
|
||||
pauseMs: 250,
|
||||
headless: true,
|
||||
screenshotDir: "output/web-game",
|
||||
actionsFile: null,
|
||||
actionsJson: null,
|
||||
click: null,
|
||||
clickSelector: null,
|
||||
};
|
||||
for (let i = 2; i < argv.length; i++) {
|
||||
const arg = argv[i];
|
||||
const next = argv[i + 1];
|
||||
if (arg === "--url" && next) {
|
||||
args.url = next;
|
||||
i++;
|
||||
} else if (arg === "--iterations" && next) {
|
||||
args.iterations = parseInt(next, 10);
|
||||
i++;
|
||||
} else if (arg === "--pause-ms" && next) {
|
||||
args.pauseMs = parseInt(next, 10);
|
||||
i++;
|
||||
} else if (arg === "--headless" && next) {
|
||||
args.headless = next !== "0" && next !== "false";
|
||||
i++;
|
||||
} else if (arg === "--screenshot-dir" && next) {
|
||||
args.screenshotDir = next;
|
||||
i++;
|
||||
} else if (arg === "--actions-file" && next) {
|
||||
args.actionsFile = next;
|
||||
i++;
|
||||
} else if (arg === "--actions-json" && next) {
|
||||
args.actionsJson = next;
|
||||
i++;
|
||||
} else if (arg === "--click" && next) {
|
||||
const parts = next.split(",").map((v) => parseFloat(v.trim()));
|
||||
if (parts.length === 2 && parts.every((v) => Number.isFinite(v))) {
|
||||
args.click = { x: parts[0], y: parts[1] };
|
||||
}
|
||||
i++;
|
||||
} else if (arg === "--click-selector" && next) {
|
||||
args.clickSelector = next;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
if (!args.url) {
|
||||
throw new Error("--url is required");
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
const buttonNameToKey = {
|
||||
up: "ArrowUp",
|
||||
down: "ArrowDown",
|
||||
left: "ArrowLeft",
|
||||
right: "ArrowRight",
|
||||
enter: "Enter",
|
||||
space: "Space",
|
||||
a: "KeyA",
|
||||
b: "KeyB",
|
||||
};
|
||||
|
||||
async function sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
function ensureDir(p) {
|
||||
fs.mkdirSync(p, { recursive: true });
|
||||
}
|
||||
|
||||
function makeVirtualTimeShim() {
|
||||
return `(() => {
|
||||
const pending = new Set();
|
||||
const origSetTimeout = window.setTimeout.bind(window);
|
||||
const origSetInterval = window.setInterval.bind(window);
|
||||
const origRequestAnimationFrame = window.requestAnimationFrame.bind(window);
|
||||
|
||||
window.__vt_pending = pending;
|
||||
|
||||
window.setTimeout = (fn, t, ...rest) => {
|
||||
const task = {};
|
||||
pending.add(task);
|
||||
return origSetTimeout(() => {
|
||||
pending.delete(task);
|
||||
fn(...rest);
|
||||
}, t);
|
||||
};
|
||||
|
||||
window.setInterval = (fn, t, ...rest) => {
|
||||
const task = {};
|
||||
pending.add(task);
|
||||
return origSetInterval(() => {
|
||||
fn(...rest);
|
||||
}, t);
|
||||
};
|
||||
|
||||
window.requestAnimationFrame = (fn) => {
|
||||
const task = {};
|
||||
pending.add(task);
|
||||
return origRequestAnimationFrame((ts) => {
|
||||
pending.delete(task);
|
||||
fn(ts);
|
||||
});
|
||||
};
|
||||
|
||||
window.advanceTime = (ms) => {
|
||||
return new Promise((resolve) => {
|
||||
const start = performance.now();
|
||||
function step(now) {
|
||||
if (now - start >= ms) return resolve();
|
||||
origRequestAnimationFrame(step);
|
||||
}
|
||||
origRequestAnimationFrame(step);
|
||||
});
|
||||
};
|
||||
|
||||
window.__drainVirtualTimePending = () => pending.size;
|
||||
})();`;
|
||||
}
|
||||
|
||||
async function getCanvasHandle(page) {
|
||||
const handle = await page.evaluateHandle(() => {
|
||||
let best = null;
|
||||
let bestArea = 0;
|
||||
for (const canvas of document.querySelectorAll("canvas")) {
|
||||
const area = (canvas.width || canvas.clientWidth || 0) * (canvas.height || canvas.clientHeight || 0);
|
||||
if (area > bestArea) {
|
||||
bestArea = area;
|
||||
best = canvas;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
});
|
||||
return handle.asElement();
|
||||
}
|
||||
|
||||
async function captureCanvasPngBase64(canvas) {
|
||||
return canvas.evaluate((c) => {
|
||||
if (!c || typeof c.toDataURL !== "function") return "";
|
||||
const data = c.toDataURL("image/png");
|
||||
const idx = data.indexOf(",");
|
||||
return idx === -1 ? "" : data.slice(idx + 1);
|
||||
});
|
||||
}
|
||||
|
||||
async function isCanvasTransparent(canvas) {
|
||||
if (!canvas) return true;
|
||||
return canvas.evaluate((c) => {
|
||||
try {
|
||||
const w = c.width || c.clientWidth || 0;
|
||||
const h = c.height || c.clientHeight || 0;
|
||||
if (!w || !h) return true;
|
||||
const size = Math.max(1, Math.min(16, w, h));
|
||||
const probe = document.createElement("canvas");
|
||||
probe.width = size;
|
||||
probe.height = size;
|
||||
const ctx = probe.getContext("2d");
|
||||
if (!ctx) return true;
|
||||
ctx.drawImage(c, 0, 0, size, size);
|
||||
const data = ctx.getImageData(0, 0, size, size).data;
|
||||
for (let i = 3; i < data.length; i += 4) {
|
||||
if (data[i] !== 0) return false;
|
||||
}
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function captureScreenshot(page, canvas, outPath) {
|
||||
let buffer = null;
|
||||
let base64 = canvas ? await captureCanvasPngBase64(canvas) : "";
|
||||
if (base64) {
|
||||
buffer = Buffer.from(base64, "base64");
|
||||
const transparent = canvas ? await isCanvasTransparent(canvas) : false;
|
||||
if (transparent) buffer = null;
|
||||
}
|
||||
if (!buffer && canvas) {
|
||||
try {
|
||||
buffer = await canvas.screenshot({ type: "png" });
|
||||
} catch {
|
||||
buffer = null;
|
||||
}
|
||||
}
|
||||
if (!buffer) {
|
||||
const bbox = canvas ? await canvas.boundingBox() : null;
|
||||
if (bbox) {
|
||||
buffer = await page.screenshot({
|
||||
type: "png",
|
||||
omitBackground: false,
|
||||
clip: bbox,
|
||||
});
|
||||
} else {
|
||||
buffer = await page.screenshot({ type: "png", omitBackground: false });
|
||||
}
|
||||
}
|
||||
fs.writeFileSync(outPath, buffer);
|
||||
}
|
||||
|
||||
class ConsoleErrorTracker {
|
||||
constructor() {
|
||||
this._seen = new Set();
|
||||
this._errors = [];
|
||||
}
|
||||
|
||||
ingest(err) {
|
||||
const key = JSON.stringify(err);
|
||||
if (this._seen.has(key)) return;
|
||||
this._seen.add(key);
|
||||
this._errors.push(err);
|
||||
}
|
||||
|
||||
drain() {
|
||||
const next = [...this._errors];
|
||||
this._errors = [];
|
||||
return next;
|
||||
}
|
||||
}
|
||||
|
||||
async function doChoreography(page, canvas, steps) {
|
||||
for (const step of steps) {
|
||||
const buttons = new Set(step.buttons || []);
|
||||
for (const button of buttons) {
|
||||
if (button === "left_mouse_button" || button === "right_mouse_button") {
|
||||
const bbox = canvas ? await canvas.boundingBox() : null;
|
||||
if (!bbox) continue;
|
||||
const x = typeof step.mouse_x === "number" ? step.mouse_x : bbox.width / 2;
|
||||
const y = typeof step.mouse_y === "number" ? step.mouse_y : bbox.height / 2;
|
||||
await page.mouse.move(bbox.x + x, bbox.y + y);
|
||||
await page.mouse.down({ button: button === "left_mouse_button" ? "left" : "right" });
|
||||
} else if (buttonNameToKey[button]) {
|
||||
await page.keyboard.down(buttonNameToKey[button]);
|
||||
}
|
||||
}
|
||||
|
||||
const frames = step.frames || 1;
|
||||
for (let i = 0; i < frames; i++) {
|
||||
await page.evaluate(async () => {
|
||||
if (typeof window.advanceTime === "function") {
|
||||
await window.advanceTime(1000 / 60);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (const button of buttons) {
|
||||
if (button === "left_mouse_button" || button === "right_mouse_button") {
|
||||
await page.mouse.up({ button: button === "left_mouse_button" ? "left" : "right" });
|
||||
} else if (buttonNameToKey[button]) {
|
||||
await page.keyboard.up(buttonNameToKey[button]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = parseArgs(process.argv);
|
||||
ensureDir(args.screenshotDir);
|
||||
|
||||
const browser = await chromium.launch({
|
||||
headless: args.headless,
|
||||
args: ["--use-gl=angle", "--use-angle=swiftshader"],
|
||||
});
|
||||
const page = await browser.newPage();
|
||||
const consoleErrors = new ConsoleErrorTracker();
|
||||
|
||||
page.on("console", (msg) => {
|
||||
if (msg.type() !== "error") return;
|
||||
consoleErrors.ingest({ type: "console.error", text: msg.text() });
|
||||
});
|
||||
page.on("pageerror", (err) => {
|
||||
consoleErrors.ingest({ type: "pageerror", text: String(err) });
|
||||
});
|
||||
|
||||
await page.addInitScript({ content: makeVirtualTimeShim() });
|
||||
await page.goto(args.url, { waitUntil: "domcontentloaded" });
|
||||
await page.waitForTimeout(500);
|
||||
await page.evaluate(() => {
|
||||
window.dispatchEvent(new Event("resize"));
|
||||
});
|
||||
|
||||
let canvas = await getCanvasHandle(page);
|
||||
|
||||
if (args.clickSelector) {
|
||||
try {
|
||||
await page.click(args.clickSelector, { timeout: 5000 });
|
||||
await page.waitForTimeout(250);
|
||||
} catch (err) {
|
||||
console.warn("Failed to click selector", args.clickSelector, err);
|
||||
}
|
||||
}
|
||||
let steps = null;
|
||||
if (args.actionsFile) {
|
||||
const raw = fs.readFileSync(args.actionsFile, "utf-8");
|
||||
const parsed = JSON.parse(raw);
|
||||
if (Array.isArray(parsed)) steps = parsed;
|
||||
if (parsed && Array.isArray(parsed.steps)) steps = parsed.steps;
|
||||
} else if (args.actionsJson) {
|
||||
const parsed = JSON.parse(args.actionsJson);
|
||||
if (Array.isArray(parsed)) steps = parsed;
|
||||
if (parsed && Array.isArray(parsed.steps)) steps = parsed.steps;
|
||||
} else if (args.click) {
|
||||
steps = [
|
||||
{
|
||||
buttons: ["left_mouse_button"],
|
||||
frames: 2,
|
||||
mouse_x: args.click.x,
|
||||
mouse_y: args.click.y,
|
||||
},
|
||||
];
|
||||
}
|
||||
if (!steps) {
|
||||
throw new Error("Actions are required. Use --actions-file, --actions-json, or --click.");
|
||||
}
|
||||
|
||||
for (let i = 0; i < args.iterations; i++) {
|
||||
if (!canvas) canvas = await getCanvasHandle(page);
|
||||
await doChoreography(page, canvas, steps);
|
||||
await sleep(args.pauseMs);
|
||||
|
||||
const shotPath = path.join(args.screenshotDir, `shot-${i}.png`);
|
||||
await captureScreenshot(page, canvas, shotPath);
|
||||
|
||||
const text = await page.evaluate(() => {
|
||||
if (typeof window.render_game_to_text === "function") {
|
||||
return window.render_game_to_text();
|
||||
}
|
||||
return null;
|
||||
});
|
||||
if (text) {
|
||||
fs.writeFileSync(path.join(args.screenshotDir, `state-${i}.json`), text);
|
||||
}
|
||||
|
||||
const freshErrors = consoleErrors.drain();
|
||||
if (freshErrors.length) {
|
||||
fs.writeFileSync(
|
||||
path.join(args.screenshotDir, `errors-${i}.json`),
|
||||
JSON.stringify(freshErrors, null, 2)
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user