commit:8894f3d8b1c951fac63dc176f3ba000b338dd534
author:Chip Black
committer:Chip Black
date:Thu Oct 18 00:05:23 2018 -0500
parents:1cdf51587e49325e72a6d2a750cdad6679234ee5
Add scripting engine
diff --git a/src/script.ts b/src/script.ts
line changes: +216/-0
index 0000000..8b455a4
--- /dev/null
+++ b/src/script.ts
@@ -0,0 +1,216 @@
+import gs from './gamestate';
+
+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();
+    }
+}
+
+export class Script {
+    instructions: ScriptInstruction[]
+    pc: number
+
+    constructor(text?: string) {
+        this.instructions = [];
+        this.pc = 0;
+        if (text)
+            this.parse(text);
+    }
+
+    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;
+    }
+
+    step(): Promise<void> {
+        let r = this.instructions[this.pc].execute();
+        this.pc++;
+        return r;
+    }
+
+    async execute() {
+        this.pc = 0;
+        while (this.pc < this.instructions.length) {
+            await this.step();
+        }
+    }
+
+    dump() {
+        console.log(this.instructions);
+    }
+}
+
+export class ScriptEngine {
+    
+}