397 lines
9.9 KiB
JavaScript
397 lines
9.9 KiB
JavaScript
import * as tools from "./tools.js";
|
||
import * as Resource from "./ontology/resource.js";
|
||
import * as Games from "./games.js";
|
||
import * as Sprite from "./ontology/vgdl-sprite.js";
|
||
import * as Constants from "./ontology/constants.js";
|
||
import * as Avatars from "./ontology/avatar.js";
|
||
import * as Termination from "./ontology/termination.js";
|
||
import * as Condition from "./ontology/conditional.js";
|
||
import * as Effect from "./ontology/effect.js";
|
||
import * as Physics from "./ontology/physics.js";
|
||
import { killSprite, scoreChange } from "./ontology/effect.js";
|
||
import { BasicGame } from "./games.js";
|
||
|
||
const EVALS = {
|
||
True: true,
|
||
False: false,
|
||
};
|
||
|
||
for (const x of [
|
||
tools,
|
||
Games,
|
||
Sprite,
|
||
Constants,
|
||
Avatars,
|
||
Termination,
|
||
Condition,
|
||
Effect,
|
||
Physics,
|
||
Resource,
|
||
]) {
|
||
const constantKeys = Object.keys(x);
|
||
|
||
// 遍历属性并添加到字典中
|
||
constantKeys.forEach((key) => {
|
||
EVALS[key] = x[key];
|
||
});
|
||
}
|
||
|
||
export class Node {
|
||
constructor(content, indent, parent = null, line = 0) {
|
||
this.children = [];
|
||
this.content = content;
|
||
this.indent = indent;
|
||
this.parent = null;
|
||
this.line = line;
|
||
if (parent) parent.insert(this);
|
||
}
|
||
|
||
insert = (node) => {
|
||
if (this.indent < node.indent) {
|
||
this.children.push(node);
|
||
node.parent = this;
|
||
} else {
|
||
this.parent.insert(node);
|
||
}
|
||
};
|
||
|
||
getRoot = () => {
|
||
if (this.parent) return this.parent.getRoot();
|
||
else return this;
|
||
};
|
||
}
|
||
|
||
function indentTreeParser(s = "", tabsize = 8) {
|
||
s = s.replace("\t", " ".repeat(tabsize));
|
||
s = s.replace("(", " ").replace(")", " ").replace(",", " ");
|
||
let lines = s.split("\n");
|
||
|
||
let last = new Node("", -1);
|
||
|
||
const root = last;
|
||
|
||
for (let i = 0; i < lines.length; i++) {
|
||
let line = lines[i];
|
||
if (line.includes("#")) {
|
||
line = line.split("#")[0];
|
||
}
|
||
|
||
const content = line.trim();
|
||
|
||
if (content.length > 0) {
|
||
const indent = line.length - line.trimStart().length;
|
||
last = new Node(content, indent, last, i);
|
||
}
|
||
}
|
||
|
||
// for (let line of lines) {
|
||
// if(line.includes("#")){
|
||
// line = line.split("#")[0]
|
||
// }
|
||
|
||
// const content = line.trim()
|
||
|
||
// if(content.length > 0){
|
||
// const indent = line.length - line.trimStart().length
|
||
// last = new Node(content, indent, last)
|
||
// }
|
||
// }
|
||
return root;
|
||
}
|
||
|
||
function _eval(estr) {
|
||
if (estr === "True") return true;
|
||
if (estr === "False") return false;
|
||
try {
|
||
return eval(estr);
|
||
} catch (e) {
|
||
if (!EVALS.hasOwnProperty(estr)) throw e;
|
||
return eval(`EVALS.${estr}`);
|
||
}
|
||
}
|
||
|
||
export class VGDLParser {
|
||
game = null;
|
||
images = ["error.png"];
|
||
ignore_colors = ["wall", "avatar"];
|
||
var_colors = {};
|
||
|
||
/**
|
||
*
|
||
* @param {string} tree
|
||
* @param {number} seed
|
||
* @returns {BasicGame}
|
||
*/
|
||
|
||
parseGame = (tree, seed = null) => {
|
||
if (!(tree instanceof Node)) tree = indentTreeParser(tree);
|
||
const root = tree.children[0];
|
||
const [sclass, args] = this.parseArgs(root.content);
|
||
this.game = new sclass(args);
|
||
|
||
// Parse Parameter Set first
|
||
for (const c of root.children) {
|
||
if (c.content === "ParameterSet") {
|
||
}
|
||
}
|
||
try {
|
||
for (const c of root.children) {
|
||
switch (c.content) {
|
||
case "SpriteSet":
|
||
this.parseSprites(c.children);
|
||
// this.parseSprites(c.children)
|
||
break;
|
||
case "InteractionSet":
|
||
this.parseInteraction(c.children);
|
||
break;
|
||
case "LevelMapping":
|
||
this.parseLevel(c.children);
|
||
break;
|
||
case "TerminationSet":
|
||
this.parseTermination(c.children);
|
||
break;
|
||
case "ConditionSet":
|
||
this.parseCondition(c.children);
|
||
break;
|
||
default:
|
||
console.error(`Unknown content: ${c.content} at line ${c.line}`);
|
||
break;
|
||
}
|
||
}
|
||
} catch (e) {
|
||
throw e;
|
||
}
|
||
|
||
const keys = [];
|
||
let colors = [];
|
||
|
||
for (const key in this.var_colors) {
|
||
keys.push(key);
|
||
colors.push(this.game.sprite_constr[key][1].color);
|
||
}
|
||
|
||
if (seed) {
|
||
colors = colors.shuffled(seed);
|
||
let index = 0;
|
||
colors.forEach((c) => {
|
||
this.game.sprite_constr[keys[index]][1].color = c;
|
||
index++;
|
||
});
|
||
}
|
||
|
||
this.game.images = this.images.slice();
|
||
return this.game;
|
||
};
|
||
|
||
parseLevel = (mnodes) => {
|
||
mnodes.forEach((mnode) => {
|
||
try {
|
||
if (mnode.content.indexOf(">") === -1) {
|
||
throw "No '>' symbol in level mapping";
|
||
}
|
||
const [c, val] = mnode.content.split(">").map((x) => {
|
||
return x.trim();
|
||
});
|
||
|
||
// console.assert(
|
||
// c.length === 1,
|
||
// "Only single character mappings allowed",
|
||
// );
|
||
|
||
const keys = val.split(" ").map((x) => {
|
||
return x.trim();
|
||
});
|
||
|
||
// console.debug("Mapping", c, keys);
|
||
|
||
this.game.char_mapping[c] = keys;
|
||
} catch (e) {
|
||
throw new Error(
|
||
`Parse Level Mapping Fail at Line ${mnode.line}: \n ${mnode.content} \n ${e.toString()}`,
|
||
);
|
||
}
|
||
});
|
||
};
|
||
|
||
parseCondition = (cnodes) => {
|
||
cnodes.forEach((c) => {
|
||
if (c.content.indexOf(">") !== -1) {
|
||
const [conditional, interaction] = c.content
|
||
.split(">")
|
||
.map((s) => s.trim());
|
||
|
||
try {
|
||
const [cclass, cargs] = this.parseArgs(conditional);
|
||
const [eclass, eargs] = this.parseArgs(conditional);
|
||
|
||
// console.debug(`Adding Condition ${conditional} ${interaction}`);
|
||
|
||
this.game.conditions.push([new cclass(cargs), [eclass, eargs]]);
|
||
} catch (e) {
|
||
throw new Error(
|
||
`Parse Condition Fail at Line ${c.line}: \n ${c.content} \n ${e.toString()}`,
|
||
);
|
||
}
|
||
}
|
||
});
|
||
};
|
||
|
||
parseTermination = (tnodes) => {
|
||
tnodes.forEach((t) => {
|
||
try {
|
||
const [sclass, args] = this.parseArgs(t.content);
|
||
// console.debug(`Adding Termination: ${sclass} ${args}`);
|
||
this.game.terminations.push(new sclass(args));
|
||
} catch (e) {
|
||
throw new Error(
|
||
`Parse Termination Fail at Line ${t.line}:\n ${t.content} \n ${e.toString()}`,
|
||
);
|
||
}
|
||
});
|
||
};
|
||
|
||
parseInteraction = (inodes) => {
|
||
inodes.forEach((i) => {
|
||
if (i.content.indexOf(">") !== -1) {
|
||
try {
|
||
const [pair, edef] = i.content.split(">").map((s) => s.trim());
|
||
const [eclass, args] = this.parseArgs(edef);
|
||
|
||
//支持object为多个的情况,测试环境为surround
|
||
const pairs = pair
|
||
.split(" ")
|
||
.map((s) => s.trim())
|
||
.filter((s) => s);
|
||
|
||
const subject = pairs[0];
|
||
|
||
for (let pair_idx = 1; pair_idx < pairs.length; pair_idx++) {
|
||
const object = pairs[pair_idx];
|
||
this.game.collision_eff.push([subject, object, eclass, args]);
|
||
if (args["scoreChange"]) {
|
||
this.game.collision_eff.push([
|
||
subject,
|
||
object,
|
||
scoreChange,
|
||
{ score: args["scoreChange"] },
|
||
]);
|
||
}
|
||
|
||
if (args["killSecond"]) {
|
||
this.game.collision_eff.push([
|
||
object,
|
||
subject,
|
||
killSprite,
|
||
{ score: args["killSecond"] },
|
||
]);
|
||
}
|
||
}
|
||
|
||
// console.debug(`Adding Collision ${pair} has effect: ${edef}`);
|
||
} catch (e) {
|
||
throw new Error(
|
||
`Parse Interaction Fail at Line ${i.line}: \n ${i.content} \n ${e.toString()}`,
|
||
);
|
||
}
|
||
}
|
||
});
|
||
};
|
||
|
||
parseSprites = (
|
||
snodes,
|
||
parentClass = null,
|
||
parentargs = {},
|
||
parenttypes = [],
|
||
) => {
|
||
snodes.forEach((s) => {
|
||
try {
|
||
// console.assert(s.content.indexOf(">") !== -1);
|
||
const [key, sdef] = s.content.split(">").map((s) => s.trim());
|
||
let [sclass, args] = this.parseArgs(
|
||
sdef,
|
||
parentClass,
|
||
Object.assign({}, parentargs),
|
||
);
|
||
|
||
if ("image" in args) {
|
||
this.images.push(args.image);
|
||
}
|
||
|
||
const stypes = parenttypes.concat(key);
|
||
if ("singleton" in args) {
|
||
if (args["singleton"] === true) this.game.singletons.push(key);
|
||
args = JSON.parse(JSON.stringify(args));
|
||
delete args["singleton"];
|
||
}
|
||
|
||
if (s.children.length === 0) {
|
||
// console.debug(`Defining: ${key} ${sclass} ${args} ${stypes}`);
|
||
this.game.sprite_constr[key] = [sclass, args, stypes];
|
||
|
||
if (
|
||
args.color &&
|
||
!("color" in parentargs) &&
|
||
!this.ignore_colors.contains(key)
|
||
) {
|
||
this.var_colors[key] = args.color;
|
||
}
|
||
|
||
if (this.game.sprite_order.contains(key))
|
||
this.game.sprite_order.remove(key);
|
||
this.game.sprite_order.push(key);
|
||
|
||
let dict = this.game.objectTypes;
|
||
|
||
for (const parent of stypes) {
|
||
if (parent === key) {
|
||
break;
|
||
}
|
||
dict = dict[parent];
|
||
}
|
||
dict[key] = {};
|
||
} else {
|
||
let dict = this.game.objectTypes;
|
||
|
||
for (const parent of stypes) {
|
||
if (parent === key) {
|
||
break;
|
||
}
|
||
dict = dict[parent];
|
||
}
|
||
dict[key] = {};
|
||
|
||
this.parseSprites(s.children, sclass, args, stypes);
|
||
}
|
||
} catch (e) {
|
||
throw new Error(
|
||
`Parse Sprite Fail at Line ${s.line}: \n ${s.content} \n ${e.toString()}`,
|
||
);
|
||
}
|
||
});
|
||
};
|
||
|
||
parseArgs = (s, sclass = {}, args = {}) => {
|
||
let sparts = s
|
||
.split(" ")
|
||
.filter((c) => c.length > 0)
|
||
.map((c) => c.trim());
|
||
if (sparts.length === 0) return [sclass, args];
|
||
|
||
if (sparts[0].indexOf("=") === -1) {
|
||
sclass = _eval(sparts[0]);
|
||
sparts = sparts.slice(1);
|
||
}
|
||
|
||
sparts.forEach((spart) => {
|
||
const [k, val] = spart.split("=");
|
||
|
||
try {
|
||
args[k] = _eval(val);
|
||
} catch (e) {
|
||
args[k] = val;
|
||
}
|
||
});
|
||
return [sclass, args];
|
||
};
|
||
}
|