Use nearley parser for scripting
"lockfileVersion": 1,
"requires": true,
"dependencies": {
+ "@types/moo": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/@types/moo/-/moo-0.4.1.tgz",
+ "integrity": "sha512-09pPEdpXBwg5nqyuXLKeW8nNxKgwtjVp9IsRFiMj5qoqU3wU+4VyjaQk5W6sJ6g5GxpFm/Oep1vLfdg9kVPJ7Q=="
+ },
+ "@types/nearley": {
+ "version": "2.11.0",
+ "resolved": "https://registry.npmjs.org/@types/nearley/-/nearley-2.11.0.tgz",
+ "integrity": "sha512-sW7qB59pz+AJCpiT3ivsMMsr/5stuCUf4IC22QfhYAMItBy3XSjyzCa/oRG+oTTzYnx5VuPtPNHN4fZ0QRnKDg=="
+ },
"@webassemblyjs/ast": {
"version": "1.7.8",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.7.8.tgz",
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
"dev": true
},
+ "colors": {
+ "version": "0.5.1",
+ "resolved": "https://registry.npmjs.org/colors/-/colors-0.5.1.tgz",
+ "integrity": "sha1-fQAj6usVTo7p/Oddy5I9DtFmd3Q="
+ },
"commander": {
"version": "2.13.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.13.0.tgz",
"randombytes": "2.0.6"
}
},
+ "discontinuous-range": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz",
+ "integrity": "sha1-44Mx8IRLukm5qctxx3FYWqsbxlo="
+ },
"domain-browser": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz",
"minimist": "0.0.8"
}
},
+ "moo": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/moo/-/moo-0.4.3.tgz",
+ "integrity": "sha512-gFD2xGCl8YFgGHsqJ9NKRVdwlioeW3mI1iqfLNYQOv0+6JRwG58Zk9DIGQgyIaffSYaO1xsKnMaYzzNr1KyIAw=="
+ },
"move-concurrently": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
"to-regex": "3.0.2"
}
},
+ "nearley": {
+ "version": "2.15.1",
+ "resolved": "https://registry.npmjs.org/nearley/-/nearley-2.15.1.tgz",
+ "integrity": "sha512-8IUY/rUrKz2mIynUGh8k+tul1awMKEjeHHC5G3FHvvyAW6oq4mQfNp2c0BMea+sYZJvYcrrM6GmZVIle/GRXGw==",
+ "requires": {
+ "moo": "0.4.3",
+ "nomnom": "1.6.2",
+ "railroad-diagrams": "1.0.0",
+ "randexp": "0.4.6",
+ "semver": "5.5.1"
+ }
+ },
"neo-async": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.5.2.tgz",
}
}
},
+ "nomnom": {
+ "version": "1.6.2",
+ "resolved": "https://registry.npmjs.org/nomnom/-/nomnom-1.6.2.tgz",
+ "integrity": "sha1-hKZqJgF0QI/Ft3oY+IjszET7aXE=",
+ "requires": {
+ "colors": "0.5.1",
+ "underscore": "1.4.4"
+ }
+ },
"normalize-path": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz",
"integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=",
"dev": true
},
+ "railroad-diagrams": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz",
+ "integrity": "sha1-635iZ1SN3t+4mcG5Dlc3RVnN234="
+ },
+ "randexp": {
+ "version": "0.4.6",
+ "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.4.6.tgz",
+ "integrity": "sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==",
+ "requires": {
+ "discontinuous-range": "1.0.0",
+ "ret": "0.1.15"
+ }
+ },
"randombytes": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.6.tgz",
"ret": {
"version": "0.1.15",
"resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz",
- "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==",
- "dev": true
+ "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg=="
},
"rimraf": {
"version": "2.6.2",
"semver": {
"version": "5.5.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.5.1.tgz",
- "integrity": "sha512-PqpAxfrEhlSUWge8dwIp4tZnQ25DIOthpiaHNIthsjEFQD6EvqUKUDM7L8O2rShkFccYo1VjJR0coWfNkCubRw==",
- "dev": true
+ "integrity": "sha512-PqpAxfrEhlSUWge8dwIp4tZnQ25DIOthpiaHNIthsjEFQD6EvqUKUDM7L8O2rShkFccYo1VjJR0coWfNkCubRw=="
},
"serialize-javascript": {
"version": "1.5.0",
}
}
},
+ "underscore": {
+ "version": "1.4.4",
+ "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.4.4.tgz",
+ "integrity": "sha1-YaajIBBiKvoHljvzJSA88SI51gQ="
+ },
"union-value": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz",
"version": "1.0.0",
"description": "",
"main": "main.js",
- "dependencies": {},
+ "dependencies": {
+ "@types/moo": "^0.4.1",
+ "@types/nearley": "^2.11.0",
+ "moo": "^0.4.3",
+ "nearley": "^2.15.1"
+ },
"devDependencies": {
"awesome-typescript-loader": "^5.2.1",
"source-map-loader": "^0.2.4",
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
- "watch": "webpack --mode=development --watch"
+ "watch": "webpack --mode=development --watch",
+ "rebuild-parser": "nearleyc src/script/parser.ne --out src/script/parser.ts"
},
"author": "",
"license": "MIT"
import gs from './gamestate';
+import { Script } from './script';
function keydown(e: KeyboardEvent) {
switch(e.code) {
+import { ScriptInstruction } from './';
+import gs from '../gamestate';
+
+export class ClearScreenInstruction implements ScriptInstruction {
+ execute(): Promise<void> {
+ gs.textView.clear();
+ return Promise.resolve();
+ }
+}
-import gs from './gamestate';
+import gs from '../gamestate';
+import * as nearley from 'nearley';
+import * as parserGrammar from './parser';
-interface ScriptInstruction {
+export interface ScriptInstruction {
execute(): Promise<void>
}
-class SayInstruction implements ScriptInstruction {
- s: string
-
- constructor(s: string) {
- this.s = s;
- }
-
- execute(): Promise<void> {
- gs.textView.print(this.s);
- return Promise.resolve();
- }
-}
-
-interface QueryOption {
- label: string, instruction: ScriptInstruction
-}
-
-class QueryInstruction implements ScriptInstruction {
- options: QueryOption[]
-
- constructor(options: QueryOption[]) {
- this.options = options;
- }
-
- execute(): Promise<void> {
- return new Promise( (resolve, reject) => {
- gs.actions.clear();
- for (let o of this.options) {
- gs.actions.addAction(o.label, () => {
- gs.actions.clear();
- resolve(o.instruction.execute());
- });
- }
- });
- }
-}
-
-class SleepInstruction implements ScriptInstruction {
- duration: number
-
- constructor(duration: number) {
- this.duration = duration;
- }
-
- execute(): Promise<void> {
- return new Promise( (resolve, reject) => {
- setTimeout(resolve, this.duration * 1000);
- });
- }
-}
-
-class PauseInstruction implements ScriptInstruction {
- text: string
-
- constructor(text?: string) {
- this.text = text || 'Continue';
- }
-
- execute(): Promise<void> {
- return new Promise( (resolve, reject) => {
- gs.actions.clear();
- gs.actions.addAction(this.text, () => {
- gs.actions.clear();
- resolve();
- });
- });
- }
-}
-
-class ClearScreenInstruction implements ScriptInstruction {
- execute(): Promise<void> {
- gs.textView.clear();
- return Promise.resolve();
+// TODO: integrate this into script execution state?
+export async function executeInstructions(l: ScriptInstruction[]) {
+ for (const i of l) {
+ await i.execute()
}
}
}
parse(text: string) {
- const lines = text.split(/[\r\n]+/);
-
- let n = 0;
- var parseQuery = () => {
- const options: QueryOption[] = [];
- let tokens;
- do {
- n++;
- tokens = this.parseLine(lines[n]);
- const arr = tokens.indexOf('=>');
- if (arr == -1)
- continue;
- const label = tokens.slice(0, arr).join(' ');
- const instruction = parseTokens(tokens.slice(arr + 1));
- options.push({ label, instruction });
- } while (tokens[0] != 'endquery');
- return new QueryInstruction(options);
- }
-
- var parseTokens = (tokens: string[]) => {
- switch (tokens[0]) {
- case 'say':
- return new SayInstruction(tokens.slice(1).join(' '));
- case 'query':
- return parseQuery();
- case 'sleep':
- return new SleepInstruction(parseFloat(tokens[1]));
- case 'pause':
- return new PauseInstruction(tokens.slice(1).join(' '));
- case 'clear':
- return new ClearScreenInstruction();
- case undefined:
- // empty line
- break;
- default:
- this.dump();
- throw Error('Unrecognized instruction on line ' + n + ': ' + tokens[0]);
- }
- }
-
- while (n < lines.length) {
- const tokens = this.parseLine(lines[n]);
- const instruction = parseTokens(tokens);
- if (instruction) {
- this.instructions.push(instruction);
- }
- n++;
- }
- }
-
- parseLine(line: string): string[] {
- const tokens: string[] = [];
- let b = 0;
- let n = 0;
- const scan = (test: (c: string) => boolean) => {
- while (n < line.length && test(line[n]))
- n++;
- };
-
- const pushToken = () => {
- if (b < n)
- tokens.push(line.slice(b, n));
- };
-
- // fun function fuckery I'll forget in a fortnight
- const testFor = (r: RegExp) => (c: string) => r.test(c);
- const testForNot = (r: RegExp) => (c: string) => !r.test(c);
- const testQuote = testFor(/['"]/);
- const testNotQuote = testForNot(/['"]/);
- const testWhitespace = testFor(/\s/);
-
- scan(testWhitespace);
- b = n;
-
- while (n < line.length) {
- if (testQuote(line[n])) {
- const q = line[n];
- n++;
- b = n;
- // scan until next matching quote
- const re = new RegExp(q);
- scan(testForNot(re));
- pushToken();
- n++;
- //scan(testWhitespace);
- b = n;
- } else if (testWhitespace(line[n])) {
- pushToken();
- scan(testWhitespace);
- b = n;
- } else if (n + 1 == line.length) {
- n++;
- pushToken();
- } else {
- n++;
- }
- }
-
- return tokens;
+ const parser = new nearley.Parser(nearley.Grammar.fromCompiled(parserGrammar));
+ parser.feed(text);
+ // Feed in an extra space because our grammar requires whitespace at the end of a statement
+ parser.feed(' ');
+ this.instructions = parser.results[0];
}
step(): Promise<void> {
+import { ScriptInstruction, executeInstructions } from './';
+import gs from '../gamestate';
+
+enum MenuOptionType {
+ Item,
+ Buy,
+ Exit,
+}
+
+interface MenuOptionCommon {
+ kind: MenuOptionType
+ label: string
+}
+
+interface MenuItem extends MenuOptionCommon {
+ kind: MenuOptionType.Item
+ instructions: ScriptInstruction[]
+}
+
+interface MenuBuy extends MenuOptionCommon {
+ kind: MenuOptionType.Buy
+ object: string
+ value: number
+}
+
+interface MenuExit extends MenuOptionCommon {
+ kind: MenuOptionType.Exit
+}
+
+type MenuOption = MenuItem | MenuBuy | MenuExit
+
+export class MenuInstruction implements ScriptInstruction {
+ options: MenuOption[]
+
+ constructor(options: any[]) {
+ this.options = options.map(o => {
+ switch(o[1].value) {
+ case 'item':
+ return <MenuItem>{
+ kind: MenuOptionType.Item,
+ label: o[3],
+ instructions: o[5],
+ };
+ case 'buy':
+ return <MenuBuy>{
+ kind: MenuOptionType.Buy,
+ label: o[3][1].value, // TODO: fetch object name from object database
+ object: o[3][1].value,
+ value: o[5].value,
+ };
+ case 'exit':
+ return <MenuExit>{
+ kind: MenuOptionType.Exit,
+ label: o[3],
+ };
+ default:
+ throw new Error('Unknown menu item type: ' + o[1].value);
+ }
+ });
+ }
+
+ execute(): Promise<void> {
+ return new Promise( (resolve, reject) => {
+ gs.actions.clear();
+ for (let o of this.options) {
+ gs.actions.addAction(o.label, () => {
+ gs.actions.clear();
+ switch(o.kind) {
+ case MenuOptionType.Item:
+ resolve(executeInstructions(o.instructions));
+ break;
+ case MenuOptionType.Buy:
+ console.log('Buy', o.object, 'for', o.value);
+ resolve();
+ break;
+ case MenuOptionType.Exit:
+ resolve();
+ break;
+ }
+ });
+ }
+ });
+ }
+}
+@preprocessor typescript
+@{%
+import * as moo from 'moo';
+import { SayInstruction } from './say';
+import { MenuInstruction } from './menu';
+import { ClearScreenInstruction } from './clear';
+import { SleepInstruction } from './sleep';
+import { PauseInstruction } from './pause';
+
+const lexer = moo.compile({
+ ws: { match: /[ \t\r\n]+/, lineBreaks: true },
+ number: /[0-9]+(?:\.[0-9]+)?/,
+ keyword: ['say', 'menu', 'item', 'end', 'exit', 'goto', 'if',
+ 'finish', 'clear', 'buy', 'transact', 'setflag'],
+ label: /[a-zA-Z][a-zA-Z0-9]*:/,
+ word: /[a-zA-Z][a-zA-Z0-9]*/,
+ dqstring: /"(?:\\["\\]|[^\n"\\])*"/,
+ sqstring: /'(?:\\['\\]|[^\n'\\])*'/,
+ mlstring_start: '[[[',
+ mlstring_end: ']]]',
+ bang: '!',
+ flag_mark: '$',
+ obj_mark: '@',
+ mlcontent: /.+(?!\]\]\])/,
+});
+
+function cn(l: any[]) {
+ return l.filter(x => x);
+}
+%}
+
+@lexer lexer
+
+program -> __:? statement:* {% data => data[1] || data[0] %}
+
+statement -> "say" __ string __ {% data => new SayInstruction(data[2]) %}
+statement -> "menu" __ menuitem:* "end" __ {% data => new MenuInstruction(data[2]) %}
+statement -> %label __
+statement -> "goto" __ %word __
+statement -> "finish" __
+statement -> "clear" __ {% data => new ClearScreenInstruction() %}
+statement -> "sleep" __ %number __ {% data => new SleepInstruction(data[2]) %}
+statement -> "pause" __ string __ {% data => new PauseInstruction(data[2]) %}
+
+menuitem -> if:? "item" __ string __ program "end" __
+menuitem -> if:? "buy" __ obj_identifier __ %number __
+menuitem -> if:? "exit" __ string __
+
+if -> "if" __ %bang:? flag_identifier __
+if -> "if" __ %bang:? obj_identifier %word __
+
+flag_identifier -> %flag_mark %word
+obj_identifier -> %obj_mark %word
+
+number -> %number {% data => parseInt(data[0]) %}
+
+string -> %dqstring {% (s: any[]) => s[0].value.slice(1, -1) %}
+string -> %sqstring {% (s: any[]) => s[0].value.slice(1, -1) %}
+string -> mlstring {% id %}
+mlstring -> %mlstring_start %mlstring_end
+mlstring -> %mlstring_start %word %mlstring_end
+mlstring -> %mlstring_start %mlcontent %mlstring_end
+
+__ -> %ws {% (x: any) => null %}
+_ -> null | %ws {% (x: any) => null %}
+import { ScriptInstruction } from './';
+import gs from '../gamestate';
+
+export class PauseInstruction implements ScriptInstruction {
+ text: string
+
+ constructor(text?: string) {
+ this.text = text || 'Continue';
+ }
+
+ execute(): Promise<void> {
+ return new Promise( (resolve, reject) => {
+ gs.actions.clear();
+ gs.actions.addAction(this.text, () => {
+ gs.actions.clear();
+ resolve();
+ });
+ });
+ }
+}
+import { ScriptInstruction } from './';
+import gs from '../gamestate';
+
+export class SayInstruction implements ScriptInstruction {
+ s: string
+
+ constructor(s: string) {
+ this.s = s;
+ }
+
+ execute(): Promise<void> {
+ gs.textView.print(this.s);
+ return Promise.resolve();
+ }
+}
+
+import { ScriptInstruction } from './';
+
+export class SleepInstruction implements ScriptInstruction {
+ duration: number
+
+ constructor(duration: number) {
+ this.duration = duration;
+ }
+
+ execute(): Promise<void> {
+ return new Promise( (resolve, reject) => {
+ setTimeout(resolve, this.duration);
+ });
+ }
+}