mirror of
https://github.com/rr-/pq-cli.git
synced 2026-11-12 22:40:36 -07:00
tests: compare pqcli with upstream in node
`just test` now fetches Progress Quest web's game logic from the public site into $XDG_CACHE_HOME/pqcli/pq-web, pinned by SHA-256 so a change on the site fails loudly. Nothing from upstream is committed. tests/upstream/harness.js runs that code headless. test_upstream.py checks the word lists, MonsterTask and the other generators from 200 generator states, and three whole games of 10000 tasks each, comparing all state including the generator after every task. Without node or the fetched files, these tests are skipped.
This commit is contained in:
6 files changed
+556
-1
No files matched your search
@@ -0,0 +1,154 @@
|
||||
// Runs Progress Quest web's game logic headless, for comparing with pqcli.
|
||||
//
|
||||
// Usage: node harness.js UPSTREAM_DIR < requests.json > responses.json
|
||||
//
|
||||
// The input is a JSON list of requests; the output is a JSON list with one
|
||||
// response per request. Upstream's files are loaded from UPSTREAM_DIR
|
||||
// (see fetch.py) and are not part of this repository.
|
||||
|
||||
"use strict";
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const vm = require("vm");
|
||||
|
||||
const upstreamDir = process.argv[2];
|
||||
const store = {};
|
||||
|
||||
// Just enough browser for upstream to run without a DOM.
|
||||
global.window = {
|
||||
location: { href: "#" },
|
||||
localStorage: {
|
||||
getItem: (key) => (key in store ? store[key] : null),
|
||||
setItem: (key, value) => {
|
||||
store[key] = String(value);
|
||||
},
|
||||
removeItem: (key) => {
|
||||
delete store[key];
|
||||
},
|
||||
},
|
||||
};
|
||||
Object.defineProperty(global, "navigator", {
|
||||
value: { userAgent: "node" },
|
||||
configurable: true,
|
||||
});
|
||||
global.document = null;
|
||||
let expectedAlert = null;
|
||||
global.alert = (message) => {
|
||||
if (message === expectedAlert) return;
|
||||
throw new Error("upstream alert: " + message);
|
||||
};
|
||||
// The clock Worker is replaced by explicit ticks, see play().
|
||||
global.Worker = function () {
|
||||
this.postMessage = () => {};
|
||||
this.addEventListener = () => {};
|
||||
};
|
||||
const $ = (selector) => (selector === "#multiplayer:checked" ? { length: 0 } : null);
|
||||
$.isFunction = (obj) => typeof obj === "function";
|
||||
$.each = (obj, callback) => {
|
||||
if (Array.isArray(obj)) {
|
||||
for (let i = 0; i < obj.length; i++) {
|
||||
if (callback.call(obj[i], i, obj[i]) === false) break;
|
||||
}
|
||||
} else {
|
||||
for (const key in obj) {
|
||||
if (callback.call(obj[key], key, obj[key]) === false) break;
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
};
|
||||
$.ajax = () => ({ then: () => {}, done: () => {} });
|
||||
global.$ = $;
|
||||
|
||||
for (const name of ["config.js", "main.js", "newguy.js"]) {
|
||||
const file = path.join(upstreamDir, name);
|
||||
vm.runInThisContext(fs.readFileSync(file, "utf8"), { filename: file });
|
||||
}
|
||||
|
||||
let now = 0;
|
||||
global.timeGetTime = () => now;
|
||||
|
||||
// FormCreate() tries to load the character named in the URL. There is
|
||||
// none yet, so upstream alerts and gives up, which is fine here.
|
||||
expectedAlert = "Error loading game";
|
||||
FormCreate();
|
||||
expectedAlert = null;
|
||||
|
||||
// Without a DOM, ListBox.rows() has no table to read. WinItem picks a row
|
||||
// from it once the inventory passes 250 rows, so read the rows from game.
|
||||
Inventory.rows = () =>
|
||||
game.Inventory.map((row) => ({ firstChild: { innerText: row[0] } }));
|
||||
|
||||
function newGame(spec) {
|
||||
// Mirrors the character creation form: stats.seed is the generator state
|
||||
// the stats were rolled from, and the game continues from it.
|
||||
stats = Object.assign({}, spec.stats, { seed: spec.seed });
|
||||
traits = { Name: spec.name, Race: spec.race, Class: spec.class };
|
||||
sold();
|
||||
LoadGame(JSON.parse(store.roster)[spec.name]);
|
||||
}
|
||||
|
||||
function snapshot() {
|
||||
const bars = {};
|
||||
for (const id of ["ExpBar", "PlotBar", "QuestBar", "EncumBar", "TaskBar"]) {
|
||||
bars[id] = [game[id].position, game[id].max];
|
||||
}
|
||||
const statsOut = {};
|
||||
for (const key of K.Stats) statsOut[key] = game.Stats[key];
|
||||
return {
|
||||
caption: game.kill.replace(/\.\.\.$/, ""),
|
||||
level: game.Traits.Level,
|
||||
act: game.act,
|
||||
bars: bars,
|
||||
stats: statsOut,
|
||||
equipment: game.Equips,
|
||||
gold: game.Inventory[0][1],
|
||||
inventory: game.Inventory.slice(1),
|
||||
spells: game.Spells,
|
||||
quests: game.Quests,
|
||||
questmonster: game.questmonster,
|
||||
queue: game.queue,
|
||||
rng: randseed(),
|
||||
};
|
||||
}
|
||||
|
||||
function play(request) {
|
||||
newGame(request.spec);
|
||||
const snapshots = [];
|
||||
for (let i = 0; i < request.ticks; i++) {
|
||||
const completing = TaskBar.done();
|
||||
now += 100;
|
||||
clock.lasttick = now - 100;
|
||||
Timer1Timer();
|
||||
if (completing) {
|
||||
// copy, since game's lists keep changing after this tick
|
||||
snapshots.push(JSON.parse(JSON.stringify(snapshot())));
|
||||
if (snapshots.length >= request.tasks) break;
|
||||
}
|
||||
}
|
||||
return snapshots;
|
||||
}
|
||||
|
||||
function call(request) {
|
||||
randseed(request.rng.slice());
|
||||
if (request.questmonster !== undefined) {
|
||||
game.questmonster = request.questmonster;
|
||||
game.questmonsterindex = request.questmonsterindex;
|
||||
}
|
||||
const fn = global[request.fn];
|
||||
const result = fn.apply(null, request.args || []);
|
||||
return { result: result, rng: randseed() };
|
||||
}
|
||||
|
||||
function lists() {
|
||||
const result = {};
|
||||
for (const key of Object.keys(K)) {
|
||||
if (Array.isArray(K[key])) result[key] = K[key];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const handlers = { play: play, call: call, lists: lists };
|
||||
const requests = JSON.parse(fs.readFileSync(0, "utf8"));
|
||||
const responses = requests.map((request) => handlers[request.cmd](request));
|
||||
process.stdout.write(JSON.stringify(responses));
|
||||
Reference in new issue
Block a user