4 |
5 | throne
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/benches/wood.throne:
--------------------------------------------------------------------------------
1 | at 1 1 wood . at 1 1 wood . at 1 1 wood . at 1 1 wood . at 1 1 wood
2 |
3 | #update: {
4 | at X Y wood . + X 1 X' . + Y 1 Y' . + X'' 1 X . + Y'' 1 Y = at X' Y' fire . at X' Y'' fire . at X'' Y' fire . at X'' Y'' fire
5 | at X Y fire . + X 1 X' = at' X' Y fire
6 | at X Y fire . + Y 1 Y' = at' X Y' fire
7 | () = #process
8 | }
9 |
10 | #process: {
11 | at' X Y I = at X Y I
12 | () = ()
13 | }
--------------------------------------------------------------------------------
/js/index.js:
--------------------------------------------------------------------------------
1 | import("../pkg/index.js")
2 | .then(module => {
3 | module.init();
4 | var text = `
5 | at 0 0 wood . at 0 1 wood . at 1 1 wood . at 0 1 fire . #update
6 | #update: {
7 | at X Y wood . at X Y fire = at X Y fire
8 | () = #spread
9 | }
10 | #spread . $at X Y fire . + X 1 X' . + Y' 1 Y = at X' Y fire . at X Y' fire
11 | `;
12 | var context = module.Context.from_text(text);
13 | context.update();
14 | context.print();
15 |
16 | console.log(context.get_state());
17 | })
18 | .catch(console.error);
19 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "author": "Tobias Mansfield-Williams ",
3 | "name": "throne",
4 | "version": "0.3.0",
5 | "scripts": {
6 | "build": "rimraf dist pkg && webpack",
7 | "start": "rimraf dist pkg && webpack-dev-server --open -d",
8 | "test": "cargo test && wasm-pack test --headless"
9 | },
10 | "devDependencies": {
11 | "@wasm-tool/wasm-pack-plugin": "^1.1.0",
12 | "copy-webpack-plugin": "^5.0.3",
13 | "webpack": "^4.42.0",
14 | "webpack-cli": "^3.3.3",
15 | "webpack-dev-server": "^3.7.1",
16 | "rimraf": "^3.0.0"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 | const CopyPlugin = require("copy-webpack-plugin");
3 | const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin");
4 |
5 | const dist = path.resolve(__dirname, "dist");
6 |
7 | module.exports = {
8 | mode: "production",
9 | entry: {
10 | index: "./js/index.js"
11 | },
12 | output: {
13 | path: dist,
14 | filename: "[name].js"
15 | },
16 | devServer: {
17 | contentBase: dist,
18 | },
19 | plugins: [
20 | new CopyPlugin([
21 | path.resolve(__dirname, "static")
22 | ]),
23 |
24 | new WasmPackPlugin({
25 | crateDirectory: __dirname,
26 | }),
27 | ]
28 | };
29 |
--------------------------------------------------------------------------------
/src/core.rs:
--------------------------------------------------------------------------------
1 | use crate::matching::*;
2 | use crate::rule::Rule;
3 | use crate::state::State;
4 | use crate::string_cache::Atom;
5 | use crate::update::SideInput;
6 |
7 | use rand::{self, rngs::SmallRng};
8 |
9 | use std::vec::Vec;
10 |
11 | /// Stores the [State] and [Rules](Rule) for a [Context](crate::context::Context).
12 | #[derive(Clone)]
13 | pub struct Core {
14 | pub state: State,
15 | pub rules: Vec,
16 | pub executed_rule_ids: Vec,
17 | pub(crate) rule_repeat_count: usize,
18 | pub(crate) rng: SmallRng,
19 | pub(crate) qui_atom: Atom,
20 | }
21 |
22 | impl Core {
23 | pub fn rule_matches_state(
24 | &self,
25 | rule: &Rule,
26 | mut side_input: F,
27 | ) -> Result
28 | where
29 | F: SideInput,
30 | {
31 | Ok(rule_matches_state(rule, &mut self.state.clone(), &mut side_input)?.is_some())
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Tobias Mansfield-Williams
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "throne"
3 | version = "0.4.1"
4 | description = "Scripting language for game prototyping and story logic"
5 | authors = ["Tobias Mansfield-Williams "]
6 | repository = "https://github.com/t-mw/throne"
7 | readme = "README.md"
8 | keywords = ["script", "scripting", "game", "language"]
9 | categories = ["game-development"]
10 | license = "MIT"
11 | edition = "2018"
12 | exclude = ["todo.org"]
13 |
14 | [lib]
15 | crate-type = ["cdylib", "rlib"]
16 |
17 | [dependencies]
18 | itertools = "0.10"
19 | lazy_static = "1.4"
20 | pest = "2.1"
21 | pest_derive = "2.1"
22 | rand = { version = "0.8", features = ["small_rng"] }
23 | regex = "1.4"
24 |
25 | [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
26 | criterion = "0.3"
27 | minifb = "0.19"
28 | pretty_assertions = "0.7"
29 |
30 | [target.wasm32-unknown-unknown.dependencies]
31 | console_error_panic_hook = "0.1"
32 | getrandom = { version = "0.2", features = ["js"] }
33 | js-sys = "0.3"
34 | wasm-bindgen = "0.2"
35 |
36 | [target.wasm32-unknown-unknown.dev-dependencies]
37 | wasm-bindgen-test = "0.3"
38 |
39 | [[bench]]
40 | name = "benchmark"
41 | harness = false
42 |
--------------------------------------------------------------------------------
/src/throne.pest:
--------------------------------------------------------------------------------
1 | WHITESPACE = _{ " " }
2 | COMMENT = _{ ("//" ~ (!NEWLINE ~ ANY)*) | ("/*" ~ (!"*/" ~ ANY)* ~ "*/") }
3 | atom = @{ (ASCII_ALPHANUMERIC | "_" | "-")+ ~ "'"* }
4 | atom_var = @{ ASCII_ALPHA_UPPER ~ (ASCII_ALPHA_UPPER | ASCII_DIGIT | "_" | "-")* ~ "'"* }
5 | string = { "\"" ~ (!"\"" ~ ANY)* ~ "\"" }
6 | wildcard = @{ "_" ~ atom? }
7 | qui = { "qui" }
8 |
9 | // list <= and >= first, to give precendence over < and >
10 | prefix = { !(wildcard | string | atom_var | atom) ~ ("+" | "-" | "%" | "<=" | ">=" | "<" | ">" | "#" | "==") }
11 |
12 | phrase_compound = { prefix? ~ ((wildcard | string | atom_var | atom)+ | "(" ~ phrase_compound+ ~ ")") }
13 | phrase = !{ phrase_compound+ }
14 |
15 | stage_phrase = ${ "#" ~ phrase }
16 | copy_phrase = ${ "$" ~ phrase }
17 | side_phrase = ${ "^" ~ phrase }
18 | negate_phrase = ${ "!" ~ phrase }
19 | nonconsuming_phrase = ${ "?" ~ phrase }
20 | backwards_phrase = ${ "<<" ~ phrase }
21 |
22 | compiler_block = _{ "[" ~ (compiler_enable_unused_warnings | compiler_disable_unused_warnings) ~ "]" }
23 | compiler_enable_unused_warnings = { "enable-unused-warnings" }
24 | compiler_disable_unused_warnings = { "disable-unused-warnings" }
25 |
26 | state_phrase = { stage_phrase | phrase }
27 | state = { state_phrase ~ (NEWLINE? ~ "." ~ NEWLINE? ~ state_phrase)* }
28 |
29 | input_phrase = { qui | stage_phrase | copy_phrase | side_phrase | negate_phrase | backwards_phrase | nonconsuming_phrase | phrase }
30 | inputs = { input_phrase ~ (NEWLINE? ~ "." ~ NEWLINE? ~ input_phrase)* }
31 | output_phrase = { qui | stage_phrase | side_phrase | phrase }
32 | outputs = { output_phrase ~ (NEWLINE? ~ "." ~ NEWLINE? ~ output_phrase)* }
33 | rule = { inputs ~ NEWLINE? ~ "=" ~ NEWLINE? ~ outputs }
34 |
35 | backwards_def_phrase = { stage_phrase | side_phrase | negate_phrase | nonconsuming_phrase | phrase }
36 | backwards_def = { backwards_phrase ~ (NEWLINE? ~ "." ~ NEWLINE? ~ backwards_def_phrase)* }
37 |
38 | prefix_block = _{ "{" ~ NEWLINE* ~ rule ~ (NEWLINE+ ~ rule)* ~ NEWLINE* ~ "}" }
39 | prefixed = { inputs ~ ":" ~ prefix_block }
40 |
41 | file = {
42 | SOI ~
43 | (NEWLINE* ~ (prefixed | rule | backwards_def | state | compiler_block))* ~
44 | NEWLINE* ~ EOI
45 | }
46 |
--------------------------------------------------------------------------------
/src/ffi.rs:
--------------------------------------------------------------------------------
1 | use crate::context::Context;
2 | use crate::string_cache::Atom;
3 | use crate::token::Phrase;
4 |
5 | use std::ffi::CStr;
6 | use std::mem::transmute;
7 | use std::os::raw::{c_char, c_void};
8 | use std::slice;
9 |
10 | #[repr(C)]
11 | pub struct CRule {
12 | id: i32,
13 | }
14 |
15 | #[no_mangle]
16 | pub extern "C" fn throne_context_create_from_text(string_ptr: *const c_char) -> *mut Context {
17 | let cstr = unsafe { CStr::from_ptr(string_ptr) };
18 | unsafe { transmute(Box::new(Context::from_text(cstr.to_str().unwrap()))) }
19 | }
20 |
21 | #[no_mangle]
22 | pub extern "C" fn throne_context_destroy(context: *mut Context) {
23 | let _drop: Box = unsafe { transmute(context) };
24 | }
25 |
26 | #[no_mangle]
27 | pub extern "C" fn throne_update(context: *mut Context) {
28 | let context = unsafe { &mut *context };
29 | context.update().unwrap();
30 | }
31 |
32 | #[no_mangle]
33 | pub extern "C" fn throne_context_string_to_atom(
34 | context: *mut Context,
35 | string_ptr: *const c_char,
36 | ) -> Atom {
37 | let context = unsafe { &mut *context };
38 | let cstr = unsafe { CStr::from_ptr(string_ptr) };
39 | context.str_to_atom(cstr.to_str().unwrap())
40 | }
41 |
42 | #[no_mangle]
43 | pub extern "C" fn throne_context_find_matching_rules(
44 | context: *mut Context,
45 | side_input: extern "C" fn(p: *const Atom, p_len: usize, data: *mut c_void) -> bool,
46 | side_input_data: *mut c_void,
47 | result_ptr: *mut CRule,
48 | result_len: usize,
49 | ) -> usize {
50 | let context = unsafe { &mut *context };
51 | let result = unsafe { slice::from_raw_parts_mut(result_ptr, result_len) };
52 |
53 | let mut side_input_p = vec![];
54 |
55 | let rules = context
56 | .find_matching_rules(|p: &Phrase| {
57 | side_input_p.clear();
58 | side_input_p.extend(p.iter().map(|t| t.atom));
59 |
60 | if side_input(side_input_p.as_ptr(), side_input_p.len(), side_input_data) {
61 | Some(vec![])
62 | } else {
63 | None
64 | }
65 | })
66 | .unwrap();
67 |
68 | let len = rules.len().min(result_len);
69 |
70 | for i in 0..len {
71 | result[i] = CRule { id: rules[i].id };
72 | }
73 |
74 | return len;
75 | }
76 |
--------------------------------------------------------------------------------
/benches/spaceopera.throne:
--------------------------------------------------------------------------------
1 | (note 0 12 mid) . (selected-instrument 2) . (note 1 4 last) . (note 2 7 mid) . (level-instruments 2 (o (o (o (o (x (o (x (o (o (o (x (o (o (o x))))))))))))))) . (level-instruments 1 (x (o (x (o x))))) . (note 2 5 first) . (note 2 11 mid) . (note 0 6 mid) . (current-beat 9) . (note 1 0 first) . (note 0 13 last) . (note 0 1 first) . (level-instruments 0 (x (o (o (o (o (x (o (o (x (o (o (x x))))))))))))) . (note 2 15 last) . (note 1 2 mid) . (note 0 9 mid) . (level-instrument-count 3)
2 |
3 | #input-place BEATPOS: {
4 | $selected-instrument INSTRUMENT . () = #clear INSTRUMENT (#input-place-after-clear BEATPOS)
5 | }
6 |
7 | #input-place-after-clear BEATPOS: {
8 | $selected-instrument INSTRUMENT . $level-instruments INSTRUMENT NOTES . !placed INSTRUMENT = place INSTRUMENT NOTES BEATPOS first . placed INSTRUMENT
9 | place INSTRUMENT (x NOTES) POS DESC . + 1 POS POS2 . % POS2 16 POS3 = place INSTRUMENT NOTES POS3 mid . note-tmp INSTRUMENT POS DESC
10 | place INSTRUMENT (o NOTES) POS DESC . !note-tmp INSTRUMENT EXISTINGPOS EXISTINGDESC = place INSTRUMENT NOTES POS DESC
11 | place INSTRUMENT (o NOTES) POS DESC . + 1 POS POS2 . % POS2 16 POS3 . $note-tmp INSTRUMENT EXISTINGPOS EXISTINGDESC = place INSTRUMENT NOTES POS3 DESC
12 | place INSTRUMENT x POS DESC = note-tmp INSTRUMENT POS last
13 | place INSTRUMENT o POS DESC = ()
14 | () = #clean-placed
15 | }
16 |
17 | #clean-placed: {
18 | placed INSTRUMENT = ()
19 | () = ()
20 | }
21 |
22 | #input-change-left . selected-instrument I . + I2 1 I . $level-instrument-count N . % I2 N I3 = selected-instrument I3
23 | #input-change-right . selected-instrument I . + I 1 I2 . $level-instrument-count N . % I2 N I3 = selected-instrument I3
24 |
25 | $current-beat BEAT . $note INSTRUMENT1 BEAT DESC1 . $note-tmp INSTRUMENT2 BEAT DESC2 = #clear INSTRUMENT2 #dummy . ^collide BEAT
26 | $current-beat BEAT . !note INSTRUMENT1 BEAT DESC1 . $note-tmp INSTRUMENT2 BEAT last = #set-tmp INSTRUMENT2
27 |
28 | $note-tmp INSTRUMENT1 BEAT DESC1 . $note-tmp INSTRUMENT2 BEAT DESC2 . !clearing-tmp-collision = #clear INSTRUMENT2 #cleared-tmp-collision . clearing-tmp-collision . ^collide BEAT
29 | #cleared-tmp-collision . clearing-tmp-collision = ()
30 |
31 | #set-beat BEAT . current-beat EXISTINGBEAT = current-beat BEAT
32 |
33 | #set-tmp INSTRUMENT: {
34 | note-tmp INSTRUMENT POS DESC = note INSTRUMENT POS DESC
35 | () = ()
36 | }
37 |
38 | #clear INSTRUMENT RETURN: {
39 | note INSTRUMENT POS DESC = ()
40 | note-tmp INSTRUMENT POS DESC = ()
41 | () = RETURN
42 | }
43 |
44 | #dummy = ()
--------------------------------------------------------------------------------
/benches/benchmark.rs:
--------------------------------------------------------------------------------
1 | extern crate throne;
2 |
3 | #[macro_use]
4 | extern crate criterion;
5 | #[macro_use]
6 | extern crate lazy_static;
7 |
8 | use criterion::Criterion;
9 |
10 | fn criterion_benchmark(c: &mut Criterion) {
11 | c.bench_function("update/context1", |b| {
12 | b.iter_with_setup(
13 | || {
14 | // only parse once, otherwise benchmark is affected
15 | lazy_static! {
16 | static ref CONTEXT: throne::Context =
17 | throne::Context::from_text(include_str!("wood.throne"))
18 | .unwrap()
19 | .with_test_rng();
20 | }
21 |
22 | CONTEXT.clone()
23 | },
24 | |mut context| {
25 | context.append_state("#update");
26 | throne::update(&mut context.core, |_: &throne::Phrase| None).unwrap();
27 | },
28 | )
29 | });
30 |
31 | c.bench_function("update/context2", |b| {
32 | b.iter_with_setup(
33 | || {
34 | // only parse once, otherwise benchmark is affected
35 | lazy_static! {
36 | static ref CONTEXT: throne::Context =
37 | throne::Context::from_text(include_str!("spaceopera.throne"))
38 | .unwrap()
39 | .with_test_rng();
40 | }
41 |
42 | CONTEXT.clone()
43 | },
44 | |mut context| {
45 | throne::update(&mut context.core, |_: &throne::Phrase| None).unwrap();
46 | },
47 | )
48 | });
49 |
50 | c.bench_function("update/context3", |b| {
51 | b.iter_with_setup(
52 | || {
53 | // only parse once, otherwise benchmark is affected
54 | lazy_static! {
55 | static ref CONTEXT: throne::Context =
56 | throne::Context::from_text(include_str!("increment.throne"))
57 | .unwrap()
58 | .with_test_rng();
59 | }
60 |
61 | CONTEXT.clone()
62 | },
63 | |mut context| {
64 | context.append_state("#increment");
65 | throne::update(&mut context.core, |_: &throne::Phrase| None).unwrap();
66 | },
67 | )
68 | });
69 | }
70 |
71 | criterion_group!(benches, criterion_benchmark);
72 | criterion_main!(benches);
73 |
--------------------------------------------------------------------------------
/src/lib.rs:
--------------------------------------------------------------------------------
1 | //! Throne is a scripting language for game prototyping and story logic.
2 | //!
3 | //! Documentation for learning the language itself can be found on [GitHub](https://github.com/t-mw/throne#reference).
4 | //!
5 | //! # Example
6 | //!
7 | //! ```
8 | //! use throne::{ContextBuilder, tokenize};
9 | //!
10 | //! // Write your script text inline or in an external file included with `include_str!(..)`
11 | //! let script = r#"
12 | //! Mary is sister of David
13 | //! Sarah is child of Mary
14 | //! Tom is child of David
15 | //!
16 | //! CHILD is child of PARENT . AUNT is sister of PARENT .
17 | //! COUSIN is child of AUNT = COUSIN is cousin of CHILD
18 | //! "#;
19 | //!
20 | //! // Build the Throne context using your script text to define the initial state and rules
21 | //! let mut context = ContextBuilder::new()
22 | //! .text(script)
23 | //! .build()
24 | //! .unwrap_or_else(|e| panic!("Failed to build Throne context: {}", e));
25 | //!
26 | //! // Execute an update step
27 | //! context.update().unwrap_or_else(|e| panic!("Throne context update failed: {}", e));
28 | //!
29 | //! // Fetch the updated state
30 | //! let state = context.core.state.get_all();
31 | //!
32 | //! // Convert a string to a Throne phrase
33 | //! let expected_state_phrase = tokenize("Sarah is cousin of Tom", &mut context.string_cache);
34 | //!
35 | //! assert_eq!(state, vec![expected_state_phrase]);
36 | //! ```
37 |
38 | #[macro_use]
39 | extern crate lazy_static;
40 | extern crate pest;
41 | #[macro_use]
42 | extern crate pest_derive;
43 | extern crate rand;
44 | extern crate regex;
45 |
46 | mod context;
47 | mod core;
48 | #[cfg(not(target_arch = "wasm32"))]
49 | mod ffi;
50 | mod matching;
51 | mod parser;
52 | mod rule;
53 | mod state;
54 | mod string_cache;
55 | #[cfg(test)]
56 | mod tests;
57 | pub mod token;
58 | mod update;
59 | #[cfg(target_arch = "wasm32")]
60 | mod wasm;
61 |
62 | pub use crate::context::{Context, ContextBuilder};
63 | pub use crate::core::Core;
64 | #[cfg(not(target_arch = "wasm32"))]
65 | #[doc(hidden)]
66 | pub use crate::ffi::*;
67 | pub use crate::rule::Rule;
68 | pub use crate::state::{PhraseId, State};
69 | pub use crate::string_cache::{Atom, StringCache};
70 | pub use crate::token::{tokenize, Phrase, PhraseGroup, PhraseString, Token};
71 | pub use crate::update::{update, SideInput};
72 | #[cfg(target_arch = "wasm32")]
73 | pub use crate::wasm::*;
74 |
75 | pub mod errors {
76 | pub use crate::matching::ExcessivePermutationError;
77 | pub use crate::parser::Error as ParserError;
78 | pub use crate::update::{Error as UpdateError, RuleRepeatError};
79 | }
80 |
--------------------------------------------------------------------------------
/src/string_cache.rs:
--------------------------------------------------------------------------------
1 | use std::collections::HashMap;
2 | use std::convert::TryInto;
3 | use std::i32;
4 |
5 | /// References a string or integer stored in a [StringCache].
6 | #[repr(C)]
7 | #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
8 | pub struct Atom {
9 | is_integer: bool,
10 | v: u32,
11 | }
12 |
13 | /// Stores the mappings between a set of [Atoms](Atom) and the primitives that they reference.
14 | #[derive(Clone, Debug)]
15 | pub struct StringCache {
16 | atom_to_str: Vec,
17 | str_to_atom: HashMap,
18 | pub(crate) wildcard_counter: i32,
19 | }
20 |
21 | impl StringCache {
22 | pub(crate) fn new() -> StringCache {
23 | StringCache {
24 | atom_to_str: vec![],
25 | str_to_atom: HashMap::new(),
26 | wildcard_counter: 0,
27 | }
28 | }
29 |
30 | /// Returns an [Atom] referencing the provided `string`, defining a new [Atom] if necessary.
31 | ///
32 | /// The string referenced by the returned [Atom] can be retrieved using [Self::atom_to_str()],
33 | /// unless the string could be converted to an integer in which case [Self::atom_to_integer()]
34 | /// must be used.
35 | pub fn str_to_atom(&mut self, string: &str) -> Atom {
36 | use std::str::FromStr;
37 |
38 | if let Some(n) = i32::from_str(string).ok() {
39 | return StringCache::integer_to_atom(n);
40 | }
41 |
42 | if let Some(atom) = self.str_to_existing_atom(string) {
43 | return atom;
44 | }
45 |
46 | let idx = self.atom_to_str.len().try_into().unwrap();
47 | let atom = Atom {
48 | is_integer: false,
49 | v: idx,
50 | };
51 |
52 | self.atom_to_str.push(string.to_string());
53 | self.str_to_atom.insert(string.to_string(), atom);
54 |
55 | atom
56 | }
57 |
58 | /// Returns an [Atom] referencing the provided `string`, only if an [Atom] was previously defined for the string.
59 | pub fn str_to_existing_atom(&self, string: &str) -> Option {
60 | self.str_to_atom.get(string).cloned()
61 | }
62 |
63 | /// Returns an [Atom] referencing the provided `integer`.
64 | pub fn integer_to_atom(integer: i32) -> Atom {
65 | Atom {
66 | is_integer: true,
67 | v: integer as u32,
68 | }
69 | }
70 |
71 | /// Returns the string referenced by the provided `atom`.
72 | pub fn atom_to_str(&self, atom: Atom) -> Option<&str> {
73 | if atom.is_integer {
74 | None
75 | } else {
76 | Some(&self.atom_to_str[atom.v as usize])
77 | }
78 | }
79 |
80 | /// Returns the integer referenced by the provided `atom`.
81 | pub fn atom_to_integer(atom: Atom) -> Option {
82 | if atom.is_integer {
83 | Some(atom.v as i32)
84 | } else {
85 | None
86 | }
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/src/rule.rs:
--------------------------------------------------------------------------------
1 | use crate::string_cache::StringCache;
2 | use crate::token::{phrase_to_string, PhraseGroup, Token};
3 |
4 | use std::marker::PhantomData;
5 |
6 | /// Represents a Throne rule.
7 | ///
8 | /// A `Rule` is uniquely identified by its `id`.
9 | /// Each input and output for a `Rule` is a [Phrase](crate::Phrase).
10 | #[derive(Clone, Debug, Eq, PartialEq)]
11 | pub struct Rule {
12 | pub id: i32,
13 | pub inputs: Vec>,
14 | pub outputs: Vec>,
15 | pub(crate) input_phrase_group_counts: Vec,
16 | pub source_span: LineColSpan,
17 | marker: PhantomData<()>,
18 | }
19 |
20 | #[derive(Copy, Clone, Debug, Eq, PartialEq)]
21 | pub struct LineColSpan {
22 | pub line_start: usize,
23 | pub line_end: usize,
24 | pub col_start: usize,
25 | pub col_end: usize,
26 | }
27 |
28 | impl Rule {
29 | pub fn to_string(&self, string_cache: &StringCache) -> String {
30 | rule_to_string_common(
31 | self.id.to_string(),
32 | &self.inputs,
33 | &self.outputs,
34 | string_cache,
35 | )
36 | }
37 | }
38 |
39 | #[derive(Clone, Debug)]
40 | pub struct RuleBuilder {
41 | pub inputs: Vec>,
42 | pub outputs: Vec>,
43 | input_phrase_group_counts: Option>,
44 | source_span: LineColSpan,
45 | }
46 |
47 | impl RuleBuilder {
48 | pub fn new(
49 | inputs: Vec>,
50 | outputs: Vec>,
51 | source_span: LineColSpan,
52 | ) -> Self {
53 | RuleBuilder {
54 | inputs,
55 | outputs,
56 | input_phrase_group_counts: None,
57 | source_span,
58 | }
59 | }
60 |
61 | pub(crate) fn input_phrase_group_counts(mut self, v: Vec) -> Self {
62 | self.input_phrase_group_counts = Some(v);
63 | self
64 | }
65 |
66 | pub fn build(mut self, id: i32) -> Rule {
67 | let input_phrase_group_counts = self
68 | .input_phrase_group_counts
69 | .take()
70 | .unwrap_or_else(|| self.inputs.iter().map(|p| p.groups().count()).collect());
71 | Rule {
72 | id,
73 | inputs: self.inputs,
74 | outputs: self.outputs,
75 | input_phrase_group_counts,
76 | source_span: self.source_span,
77 | marker: PhantomData,
78 | }
79 | }
80 |
81 | pub fn to_string(&self, string_cache: &StringCache) -> String {
82 | rule_to_string_common("?".to_string(), &self.inputs, &self.outputs, string_cache)
83 | }
84 | }
85 |
86 | fn rule_to_string_common(
87 | id_str: String,
88 | inputs: &[Vec],
89 | outputs: &[Vec],
90 | string_cache: &StringCache,
91 | ) -> String {
92 | let inputs = inputs
93 | .iter()
94 | .map(|p| phrase_to_string(p, string_cache))
95 | .collect::>()
96 | .join(" . ");
97 |
98 | let outputs = outputs
99 | .iter()
100 | .map(|p| phrase_to_string(p, string_cache))
101 | .collect::>()
102 | .join(" . ");
103 |
104 | format!("{:5}: {} = {}", id_str, inputs, outputs)
105 | }
106 |
--------------------------------------------------------------------------------
/benches/increment.throne:
--------------------------------------------------------------------------------
1 | quality foo 1
2 |
3 | #increment . quality foo1 A . + A 1 B = quality foo1 B
4 | #increment . quality foo2 A . + A 1 B = quality foo2 B
5 | #increment . quality foo3 A . + A 1 B = quality foo3 B
6 | #increment . quality foo4 A . + A 1 B = quality foo4 B
7 | #increment . quality foo5 A . + A 1 B = quality foo5 B
8 | #increment . quality foo6 A . + A 1 B = quality foo6 B
9 | #increment . quality foo7 A . + A 1 B = quality foo7 B
10 | #increment . quality foo8 A . + A 1 B = quality foo8 B
11 | #increment . quality foo9 A . + A 1 B = quality foo9 B
12 | #increment . quality foo10 A . + A 1 B = quality foo10 B
13 | #increment . quality foo11 A . + A 1 B = quality foo11 B
14 | #increment . quality foo12 A . + A 1 B = quality foo12 B
15 | #increment . quality foo13 A . + A 1 B = quality foo13 B
16 | #increment . quality foo14 A . + A 1 B = quality foo14 B
17 | #increment . quality foo15 A . + A 1 B = quality foo15 B
18 | #increment . quality foo16 A . + A 1 B = quality foo16 B
19 | #increment . quality foo17 A . + A 1 B = quality foo17 B
20 | #increment . quality foo18 A . + A 1 B = quality foo18 B
21 | #increment . quality foo19 A . + A 1 B = quality foo19 B
22 | #increment . quality foo20 A . + A 1 B = quality foo20 B
23 | #increment . quality foo21 A . + A 1 B = quality foo21 B
24 | #increment . quality foo22 A . + A 1 B = quality foo22 B
25 | #increment . quality foo23 A . + A 1 B = quality foo23 B
26 | #increment . quality foo24 A . + A 1 B = quality foo24 B
27 | #increment . quality foo25 A . + A 1 B = quality foo25 B
28 | #increment . quality foo26 A . + A 1 B = quality foo26 B
29 | #increment . quality foo A . + A 1 B = quality foo B
30 | #increment . quality foo28 A . + A 1 B = quality foo28 B
31 | #increment . quality foo29 A . + A 1 B = quality foo29 B
32 | #increment . quality foo30 A . + A 1 B = quality foo30 B
33 | #increment . quality foo31 A . + A 1 B = quality foo31 B
34 | #increment . quality foo32 A . + A 1 B = quality foo32 B
35 | #increment . quality foo33 A . + A 1 B = quality foo33 B
36 | #increment . quality foo34 A . + A 1 B = quality foo34 B
37 | #increment . quality foo35 A . + A 1 B = quality foo35 B
38 | #increment . quality foo36 A . + A 1 B = quality foo36 B
39 | #increment . quality foo37 A . + A 1 B = quality foo37 B
40 | #increment . quality foo38 A . + A 1 B = quality foo38 B
41 | #increment . quality foo39 A . + A 1 B = quality foo39 B
42 | #increment . quality foo40 A . + A 1 B = quality foo40 B
43 | #increment . quality foo41 A . + A 1 B = quality foo41 B
44 | #increment . quality foo42 A . + A 1 B = quality foo42 B
45 | #increment . quality foo43 A . + A 1 B = quality foo43 B
46 | #increment . quality foo44 A . + A 1 B = quality foo44 B
47 | #increment . quality foo45 A . + A 1 B = quality foo45 B
48 | #increment . quality foo46 A . + A 1 B = quality foo46 B
49 | #increment . quality foo47 A . + A 1 B = quality foo47 B
50 | #increment . quality foo48 A . + A 1 B = quality foo48 B
51 | #increment . quality foo49 A . + A 1 B = quality foo49 B
52 | #increment . quality foo50 A . + A 1 B = quality foo50 B
53 | #increment . quality foo51 A . + A 1 B = quality foo51 B
54 | #increment . quality foo52 A . + A 1 B = quality foo52 B
55 | #increment . quality foo53 A . + A 1 B = quality foo53 B
56 | #increment . quality foo54 A . + A 1 B = quality foo54 B
57 | #increment . quality foo55 A . + A 1 B = quality foo55 B
58 | #increment . quality foo56 A . + A 1 B = quality foo56 B
59 | #increment . quality foo57 A . + A 1 B = quality foo57 B
60 | #increment . quality foo58 A . + A 1 B = quality foo58 B
61 | #increment . quality foo59 A . + A 1 B = quality foo59 B
62 | #increment . quality foo60 A . + A 1 B = quality foo60 B
63 | #increment . quality foo61 A . + A 1 B = quality foo61 B
64 |
--------------------------------------------------------------------------------
/examples/blocks.rs:
--------------------------------------------------------------------------------
1 | #[cfg(not(target_arch = "wasm32"))]
2 | fn main() {
3 | example::main();
4 | }
5 |
6 | #[cfg(target_arch = "wasm32")]
7 | fn main() {
8 | println!("This example is not supported on WebAssembly");
9 | }
10 |
11 | #[cfg(not(target_arch = "wasm32"))]
12 | mod example {
13 | extern crate lazy_static;
14 |
15 | extern crate minifb;
16 | extern crate pest;
17 | extern crate pest_derive;
18 | extern crate rand;
19 | extern crate regex;
20 |
21 | use minifb::{Key, KeyRepeat, Window, WindowOptions};
22 |
23 | use std::{thread, time};
24 |
25 | const WIDTH: usize = 100;
26 | const HEIGHT: usize = 200;
27 |
28 | pub fn main() {
29 | let mut window = Window::new(
30 | "Test - ESC to exit",
31 | WIDTH,
32 | HEIGHT,
33 | WindowOptions::default(),
34 | )
35 | .unwrap_or_else(|e| {
36 | panic!("{}", e);
37 | });
38 |
39 | let mut context = throne::ContextBuilder::new()
40 | .text(include_str!("blocks.throne"))
41 | .build()
42 | .unwrap_or_else(|e| panic!("{}", e));
43 |
44 | let kd = context.str_to_atom("key-down");
45 | let ku = context.str_to_atom("key-up");
46 | let kp = context.str_to_atom("key-pressed");
47 | let left = context.str_to_atom("left");
48 | let right = context.str_to_atom("right");
49 | let up = context.str_to_atom("up");
50 | let down = context.str_to_atom("down");
51 |
52 | while window.is_open() && !window.is_key_down(Key::Escape) {
53 | context.push_state("#update");
54 | // context.print();
55 |
56 | let string_to_key = |s: &throne::Atom| match s {
57 | s if *s == left => Some(Key::Left),
58 | s if *s == right => Some(Key::Right),
59 | s if *s == up => Some(Key::Up),
60 | s if *s == down => Some(Key::Down),
61 | _ => None,
62 | };
63 |
64 | context
65 | .update_with_side_input(|p: &throne::Phrase| {
66 | if p.len() != 2 {
67 | return None;
68 | }
69 |
70 | match &p[0].atom {
71 | a if *a == kd => string_to_key(&p[1].atom).and_then(|k| {
72 | if window.is_key_down(k) {
73 | Some(p.to_vec())
74 | } else {
75 | None
76 | }
77 | }),
78 | a if *a == ku => string_to_key(&p[1].atom).and_then(|k| {
79 | if !window.is_key_down(k) {
80 | Some(p.to_vec())
81 | } else {
82 | None
83 | }
84 | }),
85 | a if *a == kp => string_to_key(&p[1].atom).and_then(|k| {
86 | if window.is_key_pressed(k, KeyRepeat::Yes) {
87 | Some(p.to_vec())
88 | } else {
89 | None
90 | }
91 | }),
92 | _ => None,
93 | }
94 | })
95 | .unwrap();
96 |
97 | let mut buffer: Vec = vec![0; WIDTH * HEIGHT];
98 |
99 | let is_valid_pos = |x, y| x < WIDTH as i32 && y < HEIGHT as i32;
100 |
101 | for phrase_id in context.core.state.iter() {
102 | let p = context.core.state.get(phrase_id);
103 |
104 | match (
105 | p.get(0).and_then(|t| t.as_str(&context.string_cache)),
106 | p.get(2).and_then(|t| t.as_integer()),
107 | p.get(3).and_then(|t| t.as_integer()),
108 | p.get(4).and_then(|t| t.as_integer()),
109 | ) {
110 | (Some("block-falling"), Some(x), Some(y), _)
111 | | (Some("block-set"), _, Some(x), Some(y)) => {
112 | let color = 0x00b27474;
113 |
114 | let x0 = x * 10;
115 | let x1 = x0 + 10;
116 |
117 | let y0 = y * 10;
118 | let y1 = y0 + 10;
119 |
120 | for y in y0..y1 {
121 | for x in x0..x1 {
122 | if is_valid_pos(x, y) {
123 | let idx = x + WIDTH as i32 * (HEIGHT as i32 - 1 - y);
124 | buffer[idx as usize] = color;
125 | }
126 | }
127 | }
128 | }
129 | _ => (),
130 | }
131 | }
132 |
133 | // We unwrap here as we want this code to exit if it fails. Real applications may want to handle this in a different way
134 | window.update_with_buffer(&buffer, WIDTH, HEIGHT).unwrap();
135 |
136 | thread::sleep(time::Duration::from_millis(33));
137 | }
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/examples/blocks.throne:
--------------------------------------------------------------------------------
1 | // initialize state
2 | fall-timer 5 . default-fall-timer 5 . block-id 0 . falling-shape-id 0 . max-width 10 . max-height 20
3 |
4 | // spawn a shape that will fall, if one doesn't already exist
5 | #update . !shape _X _Y _BLOCKS . falling-shape-id ID . + ID 1 ID' = falling-shape-id ID' . #new-shape
6 | #update . $shape _X _Y _BLOCKS = #input-u
7 |
8 | // define available shapes and where they spawn
9 | #new-shape . $max-height H = new-shape 4 H ((block -1 0) ((block 0 0) ((block 1 0) ((block 0 1) (cons))))) . #shape-to-blocks (#input-lr)
10 | #new-shape . $max-height H = new-shape 4 H ((block 0 -2) ((block 0 -1) ((block 0 0) ((block 0 1) (cons))))) . #shape-to-blocks (#input-lr)
11 | #new-shape . $max-height H = new-shape 4 H ((block 0 -1) ((block 0 0) ((block 0 1) ((block 1 1) (cons))))) . #shape-to-blocks (#input-lr)
12 |
13 | // this stage is the beginning of the sequence of stages that places the blocks defined by a shape.
14 | // placing these blocks is either the result of a new shape being spawned at the top of the screen,
15 | // or an existing falling shape being moved or rotated. in the second case we move and rotate by
16 | // clearing any existing blocks and spawning a new set of blocks at updated positions.
17 | #shape-to-blocks RETURN: {
18 | // clear existing falling blocks
19 | block-falling _ID _X _Y = ()
20 | // prepare to spawn new blocks defined by the shape
21 | $new-shape _X _Y BLOCKS . !BLOCKS = BLOCKS
22 | () = #shape-to-blocks-create RETURN
23 | }
24 |
25 | // this stage spawns 'falling' blocks defined by the shape
26 | #shape-to-blocks-create RETURN: {
27 | $new-shape X Y _ . (block DX DY) BLOCK . block-id ID . + ID 1 ID' . + X DX X' . + Y DY Y' = block-falling ID X' Y' . block-id ID' . BLOCK
28 | () = #shape-to-blocks-check RETURN
29 | }
30 |
31 | // this stage aborts the placement of blocks if any constraint is violated
32 | #shape-to-blocks-check RETURN . block-falling _ X Y . $block-set _ _ X Y = #shape-to-blocks-fail RETURN
33 | #shape-to-blocks-check RETURN . block-falling _ X _ . < X 0 = #shape-to-blocks-fail RETURN
34 | #shape-to-blocks-check RETURN . block-falling _ X _ . $max-width W . >= X W = #shape-to-blocks-fail RETURN
35 | #shape-to-blocks-check RETURN . () = #shape-to-blocks-ok RETURN
36 |
37 | // in this stage placement of blocks succeeded, so the new shape becomes a falling shape
38 | #shape-to-blocks-ok RETURN: {
39 | shape _ _ _ = ()
40 | new-shape X Y BLOCKS . () = shape X Y BLOCKS . RETURN
41 | }
42 |
43 | // in this stage placement of blocks from the new shape failed, so we return to the previous falling shape
44 | #shape-to-blocks-fail RETURN: {
45 | new-shape _ _ _ = ()
46 | $shape X Y BLOCKS . () = new-shape X Y BLOCKS . #shape-to-blocks RETURN
47 | }
48 |
49 | // rotate the shape if the up arrow key is pressed
50 | #input-u . ^key-pressed up = #rotate-shape
51 | #input-u . () = #input-lr
52 | #rotate-shape: {
53 | $shape X Y BLOCKS . !new-shape X Y _ = new-shape X Y BLOCKS . new-blocks (cons)
54 | new-shape X Y ((block DX DY) BLOCKS) . new-blocks BLOCKS2 . + DX2 DX 0 = new-shape X Y BLOCKS . new-blocks ((block DY DX2) BLOCKS2)
55 | new-shape X Y _ . new-blocks BLOCKS . () = new-shape X Y BLOCKS . #shape-to-blocks (#input-d)
56 | }
57 |
58 | // move the shape horizontally if the left or right arrow key is pressed
59 | #input-lr . ^key-pressed left . $shape X Y BLOCKS . - X 1 X' = new-shape X' Y BLOCKS . #shape-to-blocks (#input-d)
60 | #input-lr . ^key-pressed right . $shape X Y BLOCKS . + X 1 X' = new-shape X' Y BLOCKS . #shape-to-blocks (#input-d)
61 | #input-lr . () = #input-d
62 |
63 | // move the shape down faster than normal if the down arrow key is pressed
64 | #input-d: {
65 | ^key-down down . default-fall-timer 5 . fall-timer _ = default-fall-timer 1 . fall-timer 0
66 | ^key-up down . default-fall-timer 1 . fall-timer _ = default-fall-timer 5 . fall-timer 0
67 | () = #collision
68 | }
69 |
70 | // if any falling blocks are about to collide with any set blocks or the bottom of the screen, the
71 | // falling blocks should become set blocks.
72 | #collision: {
73 | block-falling ID X Y . + Y' 1 Y . $block-set _ _ X Y' = block-setting ID X Y
74 | block-falling ID X Y . + Y' 1 Y . < Y' 0 = block-setting ID X Y
75 | $block-setting _ _ _ . block-falling ID X' Y' = block-setting ID X' Y'
76 | $block-setting _ _ _ . shape _ _ _ = ()
77 | () = #set
78 | }
79 | #set: {
80 | block-setting ID X Y . $falling-shape-id SHAPE_ID = block-set ID SHAPE_ID X Y
81 | $max-width W . () = #score-x . score-counter W 0
82 | }
83 |
84 | // mark completed rows for clearing
85 | #score-x . score-counter X Y . + X' 1 X . $block-set _ _ X' Y = score-counter X' Y . #score-x
86 | #score-x . score-counter 0 Y = #clear . clear-y Y
87 | #score-x . $score-counter _ _ . () = #score-y
88 | #score-y . score-counter _ Y . + Y 1 Y' . $max-width W . $max-height H . < Y' H = score-counter W Y' . #score-x
89 | #score-y . score-counter _ _ . () = #fall-tick
90 |
91 | // this stage clears blocks in any completed rows
92 | #clear: {
93 | $clear-y Y . block-set _ _ _ Y = ()
94 | block-clear-move _ = ()
95 | () = #clear-move
96 | }
97 |
98 | // this stage moves down any blocks hanging in space as a result of clearing completed rows
99 | #clear-move: {
100 | $clear-y Y . block-set ID SHAPE_ID X Y' . !block-clear-move ID . > Y' Y . - Y' 1 Y'' = block-set ID SHAPE_ID X Y'' . block-clear-move ID
101 | $max-width W . clear-y _ . () = #score-x . score-counter W 0
102 | }
103 |
104 | // move blocks down every TIMER frames
105 | #fall-tick . fall-timer TIMER . >= TIMER 0 . + TIMER2 1 TIMER . >= TIMER2 0 = fall-timer TIMER2 . #clean
106 | #fall-tick . fall-timer TIMER . >= TIMER 0 . + TIMER2 1 TIMER . < TIMER2 0 . $default-fall-timer D = fall-timer D . #fall
107 | #fall . shape X Y BLOCKS . + Y' 1 Y = new-shape X Y' BLOCKS . #shape-to-blocks #clean
108 | #fall . () = #clean
109 |
110 | #clean: {
111 | cons = ()
112 | }
--------------------------------------------------------------------------------
/src/update.rs:
--------------------------------------------------------------------------------
1 | use crate::core::Core;
2 | use crate::matching;
3 | use crate::rule::Rule;
4 | use crate::state::State;
5 | use crate::token::*;
6 |
7 | use rand::seq::SliceRandom;
8 |
9 | use std::fmt;
10 |
11 | const RULE_REPEAT_LIMIT: usize = 2000;
12 |
13 | #[derive(Debug)]
14 | pub enum Error {
15 | RuleRepeatError(RuleRepeatError),
16 | ExcessivePermutationError(matching::ExcessivePermutationError),
17 | }
18 |
19 | impl Error {
20 | pub fn rule<'a>(&self, rules: &'a [Rule]) -> Option<&'a Rule> {
21 | let rule_id = match self {
22 | Self::RuleRepeatError(e) => e.rule_id,
23 | Self::ExcessivePermutationError(e) => e.rule_id,
24 | };
25 | rules.iter().find(|r| r.id == rule_id)
26 | }
27 | }
28 |
29 | impl std::error::Error for Error {}
30 |
31 | impl fmt::Display for Error {
32 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
33 | match self {
34 | Self::RuleRepeatError(e) => {
35 | write!(f, "{}", e)
36 | }
37 | Self::ExcessivePermutationError(e) => {
38 | write!(f, "{}", e)
39 | }
40 | }
41 | }
42 | }
43 |
44 | impl From for Error {
45 | fn from(e: RuleRepeatError) -> Self {
46 | Error::RuleRepeatError(e)
47 | }
48 | }
49 |
50 | impl From for Error {
51 | fn from(e: matching::ExcessivePermutationError) -> Self {
52 | Error::ExcessivePermutationError(e)
53 | }
54 | }
55 |
56 | #[derive(Debug)]
57 | pub struct RuleRepeatError {
58 | pub rule_id: i32,
59 | }
60 |
61 | impl std::error::Error for RuleRepeatError {}
62 |
63 | impl fmt::Display for RuleRepeatError {
64 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
65 | write!(
66 | f,
67 | "Script execution was abandoned since rule {} appears to be repeating infinitely.",
68 | self.rule_id
69 | )
70 | }
71 | }
72 |
73 | // https://stackoverflow.com/questions/44246722/is-there-any-way-to-create-an-alias-of-a-specific-fnmut
74 | pub trait SideInput: FnMut(&Phrase) -> Option> {}
75 | impl SideInput for F where F: FnMut(&Phrase) -> Option> {}
76 |
77 | /// A free function equivalent to [Context::update_with_side_input](crate::Context::update_with_side_input) if required to avoid lifetime conflicts.
78 | pub fn update(core: &mut Core, mut side_input: F) -> Result<(), Error>
79 | where
80 | F: SideInput,
81 | {
82 | let state = &mut core.state;
83 | let rules = &mut core.rules;
84 | let executed_rule_ids = &mut core.executed_rule_ids;
85 | let rng = &mut core.rng;
86 |
87 | core.rule_repeat_count = 0;
88 | executed_rule_ids.clear();
89 |
90 | // shuffle state so that a given rule with multiple potential
91 | // matches does not always match the same permutation of state.
92 | state.shuffle(rng);
93 |
94 | // shuffle rules so that each has an equal chance of selection.
95 | rules.shuffle(rng);
96 |
97 | // change starting rule on each iteration to introduce randomness.
98 | let mut start_rule_idx = 0;
99 |
100 | let mut quiescence = false;
101 |
102 | loop {
103 | let mut matching_rule = None;
104 | let mut rule_output_phrase_group_counts = vec![];
105 |
106 | if quiescence {
107 | state.push_with_metadata(vec![Token::new_atom(core.qui_atom, 1, 1)], 1);
108 | }
109 |
110 | for i in 0..rules.len() {
111 | let rule = &rules[(start_rule_idx + i) % rules.len()];
112 |
113 | if let Some(result) = matching::rule_matches_state(&rule, state, &mut side_input)? {
114 | matching_rule = Some(result.rule);
115 | rule_output_phrase_group_counts = result.output_phrase_group_counts;
116 | break;
117 | }
118 | }
119 |
120 | start_rule_idx += 1;
121 |
122 | if quiescence {
123 | quiescence = false;
124 |
125 | if matching_rule.is_none() {
126 | let qui_atom = core.qui_atom;
127 | assert!(
128 | state
129 | .iter()
130 | .enumerate()
131 | .filter(|&(_, p)| state.get(p)[0].atom == qui_atom)
132 | .map(|(i, _)| i)
133 | .collect::>()
134 | == vec![state.len() - 1],
135 | "expected 1 * () at end of state"
136 | );
137 |
138 | let idx = state.len() - 1;
139 | state.remove_idx(idx);
140 | state.clear_removed_tokens();
141 |
142 | return Ok(());
143 | }
144 | }
145 |
146 | if let Some(ref matching_rule) = matching_rule {
147 | if let Some(previously_executed_rule_id) = executed_rule_ids.last() {
148 | if matching_rule.id == *previously_executed_rule_id {
149 | core.rule_repeat_count += 1;
150 | if core.rule_repeat_count > RULE_REPEAT_LIMIT {
151 | Err(RuleRepeatError {
152 | rule_id: matching_rule.id,
153 | })?;
154 | }
155 | } else {
156 | core.rule_repeat_count = 0;
157 | }
158 | }
159 |
160 | executed_rule_ids.push(matching_rule.id);
161 | assert!(execute_rule(
162 | matching_rule,
163 | state,
164 | Some(&rule_output_phrase_group_counts)
165 | ));
166 | } else {
167 | quiescence = true;
168 | }
169 | }
170 | }
171 |
172 | pub(crate) fn execute_rule(
173 | rule: &Rule,
174 | state: &mut State,
175 | rule_output_phrase_group_counts: Option<&[usize]>,
176 | ) -> bool {
177 | let inputs = &rule.inputs;
178 | let outputs = &rule.outputs;
179 |
180 | for input in inputs {
181 | if input[0].is_consuming {
182 | if !state.remove_phrase(input) {
183 | return false;
184 | }
185 | }
186 | }
187 |
188 | outputs.iter().enumerate().for_each(|(output_i, output)| {
189 | if let Some(group_counts) = rule_output_phrase_group_counts {
190 | state.push_with_metadata(output.clone(), group_counts[output_i]);
191 | } else {
192 | state.push(output.clone());
193 | }
194 | });
195 |
196 | true
197 | }
198 |
--------------------------------------------------------------------------------
/todo.org:
--------------------------------------------------------------------------------
1 | *** TODO syntax to match rule only once per update / permanently. or even make matching once per update the default.
2 | - option to scope permanence by a variable
3 | - #stage: { input 3= output } evaluates max 3 times within stage, becomes:
4 | #stage: {
5 | // must be structured to keep behavior of random rule selection
6 | !tmp-counter-123 _ . input = output . tmp-counter-123 2
7 | tmp-counter-123 N . > N 0 . - N 1 N' . input = output . tmp-counter-123 N'
8 | () = #tmp-cleanup
9 | }
10 | #tmp-cleanup: { tmp-counter-123 _ = () }
11 | - could also allow integer variables to be used instead of constant e.g. #stage: { $count N . input N= output }
12 | *** TODO lispy key properties to enable pattern matching or syntax sugar for getting + modifying values by key in a record of form (record (k1 v1) (k2 v2) ...)
13 | e.g to modify player (name N) (x 10) (y 20)
14 | // This rule...
15 | player :x X . + X 1 X' = player :x X'
16 | // ...would be syntactic sugar for:
17 | player ...O1 (x X) ...O2 . + X 1 X' = player ...O1 (x X') ...O2
18 | // if there’s more than one ‘player’ on each side of the rule the verbose syntax is needed to disambiguate.
19 | // NB: the ... proposal doesnt handle multiple unordered properties well
20 | *** TODO make $ remove instead of preserve, since remove is less common and this makes the stage behavior (#asdf:) consistent with other syntax
21 | *** TODO support custom backwards predicate (<<) under stage label
22 | *** TODO decide on consistent syntax for arguments e.g. keywords?
23 | *** TODO arbitrary math expressions e.g. 'expr 2 + A / 3 + 1.5 = B'
24 | *** TODO replace !== backwards predicate with ability to use '!' anywhere in a phrase e.g. !== A B would become == A !B
25 | *** TODO ability to arbitrarily nest 'stage' scopes
26 | *** TODO ability to put rule output in 'stage' scopes e.g. in1 . in2 = out1 { subset = () } === in1 . in2 . subset = out1
27 | *** TODO ability to put state in 'stage' scopes e.g. #stage { some-state } becomes #stage . !some-state = some-state
28 | *** TODO reduce serialization boilerplate with either serde or generated code (like with pest)
29 | *** TODO allow output in custom backwards predicate, i.e. output is appended to rule
30 | *** TODO test doing initial matching through ecs instead of first atoms (e.g. https://github.com/TomGillen/legion)
31 | - requires a macro to generate rust structs for throne identifiers
32 | - an item of state is then e.g. &[FooAtom::N1, Var::N2, BarAtom::N3, etc..]
33 | - possible matching rules are found using a component query
34 | *** TODO support marking state phrases immutable in debug builds, logging warning when the state is consumed by a rule. or do compile-time check.
35 | *** TODO support matching phrases while binding to them, to avoid retyping them in output e.g. (foo A)@FOO . bar = FOO
36 | *** TODO look at prolog optimizations
37 | - could we compile each rule down to a specialized routine with a stack for variable matches to replace test_inputs_with_permutation
38 | - bin each phrase by (atom, atom position) and variable name to speed up matching
39 | *** TODO add syntax for matching on const lists i.e. (FOO | BAR) matched against (a b c): FOO = a, BAR = (b c)
40 | *** TODO ink -> throne converter
41 | *** TODO syntax for inserting the result of an input match directly into output
42 | - e.g. foo FOO = `foo capitalized is` (^capitalize FOO (pattern: [Option<&Atom>; N], match_pattern_length: bool) -> Vec<&[Token]> { ... }`
52 | - replace find_phrase* variants too
53 | *** TODO measure performance with https://github.com/bodil/smartstring
54 | *** TODO add examples
55 | - [X] Conway's game of life
56 | - [ ] Chess
57 | - [ ] Tic tac toe
58 | - [ ] Procedural generationn
59 | *** TODO test with https://github.com/yuulive/mo
60 | *** TODO reduce permutation space beyond first atom
61 | - a X . a X = ... or a X . b X = ... with a (0..N) and b (0..N) triggers an O(N^2) search on each update.
62 | *** TODO support backwards predicates in any order
63 | - currently backwards predicates are evaluated left to right in two passes, so > 2 backwards predicates in the wrong order will fail matching e.g. + C 3 D . % B 2 C . + A 1 B = ...
64 | - backwards predicates need to be evaluated in order based on variable dependencies.
65 | - could extend ordering based on dependencies to matching in general, including side predicates and normal state matches, to reduce permutations.
66 | *** TODO try https://twitter.com/tomaka17/status/1391052081272967170
67 | - "you might be able to save a lot of hashmap lookups if you replace a `HashMap` with a `HashMap` and a `Slab`. This might be very useful if K is something heavy such as a `String`"
68 | *** DONE support quiescence rule under stage label i.e. don't copy left-hand stage for quiescence rule
69 | CLOSED: [2021-04-30 Fri 11:16]
70 | *** DONE replace #foo -> stage foo, because # does not have special effects like other symbols
71 | CLOSED: [2021-04-29 Thu 15:24]
72 | *** DONE syntax for scheduling some output of a rule to be deleted at the end of the update
73 | CLOSED: [2021-05-08 Sat 03:24]
74 | - left up to embedder.
75 | *** DONE make () = () optional in prefixed blocks
76 | CLOSED: [2021-05-04 Tue 19:55]
77 | *** DONE detect infinite loops
78 | CLOSED: [2021-05-03 Mon 13:17]
79 | *** DONE selectively disable warnings
80 | CLOSED: [2020-01-24 Fri 14:24]
81 | *** DONE wildcard variable: _
82 | *** DONE support defining own backwards predicates:
83 | - defined as rule without '=': {
26 | text: &'a str,
27 | string_cache: StringCache,
28 | rng: Option<&'a mut SmallRng>,
29 | }
30 |
31 | impl<'a> ContextBuilder<'a> {
32 | pub fn new() -> Self {
33 | ContextBuilder {
34 | text: "",
35 | string_cache: StringCache::new(),
36 | rng: None,
37 | }
38 | }
39 |
40 | /// Sets the script text used to define the initial state and rules of the [Context].
41 | pub fn text(mut self, text: &'a str) -> Self {
42 | self.text = text;
43 | self
44 | }
45 |
46 | /// Sets the [StringCache] used for the [Context].
47 | pub fn string_cache(mut self, string_cache: StringCache) -> Self {
48 | self.string_cache = string_cache;
49 | self
50 | }
51 |
52 | /// Sets the random number generator used for the [Context].
53 | ///
54 | /// Defaults to `rand::rngs::SmallRng::from_rng(&mut rand::thread_rng())`.
55 | pub fn rng(mut self, rng: &'a mut SmallRng) -> Self {
56 | self.rng = Some(rng);
57 | self
58 | }
59 |
60 | /// Builds the [Context] using the provided script text to define the initial state and rules.
61 | /// Returns an error if the script text could not be parsed.
62 | pub fn build(self) -> Result {
63 | Context::new(
64 | self.text,
65 | self.string_cache,
66 | self.rng.unwrap_or(&mut default_rng()),
67 | )
68 | }
69 | }
70 |
71 | fn default_rng() -> SmallRng {
72 | // NB: update doc for ContextBuilder::rng if this changes
73 | SmallRng::from_rng(&mut thread_rng()).unwrap()
74 | }
75 |
76 | /// Stores the [State], [Rules](Rule) and [Atom] mappings for a Throne script.
77 | ///
78 | /// Create a new `Context` using a [ContextBuilder].
79 | #[derive(Clone)]
80 | #[non_exhaustive]
81 | pub struct Context {
82 | pub core: Core,
83 | pub string_cache: StringCache,
84 | }
85 |
86 | impl Context {
87 | pub(crate) fn from_text(text: &str) -> Result {
88 | ContextBuilder::new().text(text).build()
89 | }
90 |
91 | #[cfg(test)]
92 | pub(crate) fn from_text_rng(text: &str, rng: &mut SmallRng) -> Result {
93 | ContextBuilder::new().text(text).rng(rng).build()
94 | }
95 |
96 | fn new(
97 | text: &str,
98 | mut string_cache: StringCache,
99 | rng: &mut SmallRng,
100 | ) -> Result {
101 | let result = parser::parse(text, &mut string_cache, rng)?;
102 |
103 | let mut state = State::new();
104 | for phrase in result.state.into_iter() {
105 | state.push(phrase);
106 | }
107 |
108 | let qui_atom = string_cache.str_to_atom(parser::QUI);
109 |
110 | Ok(Context {
111 | core: Core {
112 | state,
113 | rules: result.rules,
114 | executed_rule_ids: vec![],
115 | rule_repeat_count: 0,
116 | rng: rng.clone(),
117 | qui_atom,
118 | },
119 | string_cache,
120 | })
121 | }
122 |
123 | /// Executes any [Rule] that matches the current [State] until the set of matching rules is exhausted.
124 | pub fn update(&mut self) -> Result<(), update::Error> {
125 | self.update_with_side_input(|_: &Phrase| None)
126 | }
127 |
128 | /// Equivalent to [Context::update()], but accepts a callback to respond to `^` predicates.
129 | pub fn update_with_side_input(&mut self, side_input: F) -> Result<(), update::Error>
130 | where
131 | F: SideInput,
132 | {
133 | update(&mut self.core, side_input)
134 | }
135 |
136 | /// Executes a specific [Rule].
137 | ///
138 | /// Returns `true` if the [Rule] was successfully executed or `false` if some of its inputs could not be matched to the current [State].
139 | pub fn execute_rule(&mut self, rule: &Rule) -> bool {
140 | update::execute_rule(rule, &mut self.core.state, None)
141 | }
142 |
143 | /// Returns the set of [Rules](Rule) that may be executed in the next update.
144 | pub fn find_matching_rules(
145 | &self,
146 | mut side_input: F,
147 | ) -> Result, ExcessivePermutationError>
148 | where
149 | F: SideInput,
150 | {
151 | let state = &mut self.core.state.clone();
152 |
153 | let mut rules = vec![];
154 | for rule in &self.core.rules {
155 | if let Some(matching_rule) =
156 | rule_matches_state(&rule, state, &mut side_input)?.map(|result| result.rule)
157 | {
158 | rules.push(matching_rule);
159 | }
160 | }
161 |
162 | Ok(rules)
163 | }
164 |
165 | /// Alias for [StringCache::str_to_atom].
166 | pub fn str_to_atom(&mut self, string: &str) -> Atom {
167 | self.string_cache.str_to_atom(string)
168 | }
169 |
170 | /// Alias for [StringCache::str_to_existing_atom].
171 | pub fn str_to_existing_atom(&self, string: &str) -> Option {
172 | self.string_cache.str_to_existing_atom(string)
173 | }
174 |
175 | /// Alias for [StringCache::atom_to_str].
176 | pub fn atom_to_str(&self, atom: Atom) -> Option<&str> {
177 | self.string_cache.atom_to_str(atom)
178 | }
179 |
180 | /// Alias for [StringCache::atom_to_integer].
181 | pub fn atom_to_integer(&self, atom: Atom) -> Option {
182 | StringCache::atom_to_integer(atom)
183 | }
184 |
185 | #[cfg(test)]
186 | pub fn with_test_rng(mut self) -> Context {
187 | self.core.rng = crate::tests::test_rng();
188 | self
189 | }
190 |
191 | /// Converts the provided text to a [Phrase] and adds it to the context's [State].
192 | pub fn push_state(&mut self, phrase_text: &str) {
193 | self.core
194 | .state
195 | .push(tokenize(phrase_text, &mut self.string_cache));
196 | }
197 |
198 | /// Copies the state from another `Context` to this one.
199 | pub fn extend_state_from_context(&mut self, other: &Context) {
200 | for phrase_id in other.core.state.iter() {
201 | let phrase = other.core.state.get(phrase_id);
202 | let new_phrase = phrase
203 | .iter()
204 | .map(|t| {
205 | if StringCache::atom_to_integer(t.atom).is_some() {
206 | t.clone()
207 | } else {
208 | let string = other
209 | .string_cache
210 | .atom_to_str(t.atom)
211 | .expect(&format!("missing token: {:?}", t));
212 |
213 | let mut new_token = t.clone();
214 | new_token.atom = self.string_cache.str_to_atom(string);
215 | new_token
216 | }
217 | })
218 | .collect();
219 | self.core.state.push(new_phrase);
220 | }
221 | }
222 |
223 | /// Prints a representation of the `Context` to the console.
224 | pub fn print(&self) {
225 | println!("{}", self);
226 | }
227 | }
228 |
229 | impl fmt::Display for Context {
230 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
231 | let state = state::state_to_string(&self.core.state, &self.string_cache);
232 |
233 | let mut rules = self
234 | .core
235 | .rules
236 | .iter()
237 | .map(|r| r.to_string(&self.string_cache))
238 | .collect::>();
239 | rules.sort();
240 |
241 | write!(
242 | f,
243 | "state:\n{}\nrules:\n{}\n{}",
244 | state,
245 | rules.join("\n"),
246 | if self.core.executed_rule_ids.is_empty() {
247 | "no rules were executed in the previous update".to_string()
248 | } else {
249 | format!(
250 | "rule ids executed in the previous update:\n{}",
251 | self.core.executed_rule_ids.iter().join(", ")
252 | )
253 | }
254 | )
255 | }
256 | }
257 |
--------------------------------------------------------------------------------
/src/wasm.rs:
--------------------------------------------------------------------------------
1 | use crate::context::Context as ThroneContext;
2 | use crate::parser;
3 | use crate::rule::{self, Rule};
4 | use crate::string_cache::{Atom, StringCache};
5 | use crate::token::{Phrase, PhraseGroup, Token};
6 | use crate::update::{self, update};
7 |
8 | use wasm_bindgen::prelude::*;
9 |
10 | use std::collections::hash_map::DefaultHasher;
11 | use std::hash::{Hash, Hasher};
12 |
13 | #[wasm_bindgen]
14 | pub fn init() {
15 | std::panic::set_hook(Box::new(console_error_panic_hook::hook));
16 | }
17 |
18 | #[wasm_bindgen]
19 | extern "C" {
20 | #[wasm_bindgen(js_namespace = console)]
21 | fn log(s: &str);
22 | }
23 |
24 | #[wasm_bindgen]
25 | pub struct Context {
26 | throne_context: ThroneContext,
27 | }
28 |
29 | impl From for JsValue {
30 | fn from(e: parser::Error) -> Self {
31 | let js_error = js_sys::Error::new(&format!("{}", e));
32 | js_sys::Object::define_property(
33 | &js_error,
34 | &JsValue::from("throne_span"),
35 | js_sys::Object::try_from(&JsValue::from(LineColSpanDescriptor {
36 | value: e.pest.line_col.into(),
37 | }))
38 | .unwrap(),
39 | );
40 | js_error.into()
41 | }
42 | }
43 |
44 | fn js_from_update_result(result: Result<(), update::Error>, rules: &[Rule]) -> Result<(), JsValue> {
45 | match result {
46 | Err(e) => {
47 | let js_error = js_sys::Error::new(&format!("{}", e));
48 | if let Some(rule_source_span) = e.rule(rules).map(|r| r.source_span) {
49 | js_sys::Object::define_property(
50 | &js_error,
51 | &JsValue::from("throne_span"),
52 | js_sys::Object::try_from(&JsValue::from(LineColSpanDescriptor {
53 | value: rule_source_span.into(),
54 | }))
55 | .unwrap(),
56 | );
57 | }
58 | Err(js_error.into())
59 | }
60 | Ok(()) => Ok(()),
61 | }
62 | }
63 |
64 | #[wasm_bindgen]
65 | struct LineColSpanDescriptor {
66 | pub value: LineColSpan,
67 | }
68 |
69 | #[wasm_bindgen]
70 | #[derive(Copy, Clone)]
71 | pub struct LineColSpan {
72 | pub line_start: usize,
73 | pub line_end: usize,
74 | pub col_start: usize,
75 | pub col_end: usize,
76 | }
77 |
78 | impl From for LineColSpan {
79 | fn from(line_col: pest::error::LineColLocation) -> Self {
80 | match line_col {
81 | pest::error::LineColLocation::Pos((line, col)) => LineColSpan {
82 | line_start: line,
83 | line_end: line,
84 | col_start: col,
85 | col_end: col,
86 | },
87 | pest::error::LineColLocation::Span((line_start, col_start), (line_end, col_end)) => {
88 | LineColSpan {
89 | line_start,
90 | line_end,
91 | col_start,
92 | col_end,
93 | }
94 | }
95 | }
96 | }
97 | }
98 |
99 | impl From for LineColSpan {
100 | fn from(span: rule::LineColSpan) -> Self {
101 | LineColSpan {
102 | line_start: span.line_start,
103 | line_end: span.line_end,
104 | col_start: span.col_start,
105 | col_end: span.col_end,
106 | }
107 | }
108 | }
109 |
110 | #[wasm_bindgen]
111 | impl Context {
112 | pub fn from_text(text: &str) -> Result {
113 | Ok(Context {
114 | throne_context: ThroneContext::from_text(text)?,
115 | })
116 | }
117 |
118 | pub fn push_state(&mut self, text: &str) {
119 | self.throne_context.push_state(text);
120 | }
121 |
122 | pub fn remove_state_by_first_atom(&mut self, text: &str) {
123 | let atom = self.throne_context.str_to_atom(text);
124 | self.throne_context
125 | .core
126 | .state
127 | .remove_pattern([Some(atom)], false);
128 | }
129 |
130 | pub fn update_with_side_input(
131 | &mut self,
132 | side_input: Option,
133 | ) -> Result<(), JsValue> {
134 | if let Some(side_input) = side_input {
135 | let core = &mut self.throne_context.core;
136 | let string_cache = &mut self.throne_context.string_cache;
137 | let side_input = |phrase: &Phrase| {
138 | let js_phrase = js_value_from_phrase(phrase, string_cache);
139 | let result = side_input.call1(&JsValue::null(), &js_phrase);
140 | match result {
141 | Ok(v) => {
142 | if js_sys::Array::is_array(&v) {
143 | let arr = js_sys::Array::from(&v);
144 | let mut out = vec![phrase[0].clone()];
145 | for item in arr.iter().skip(1) {
146 | if let Some(s) = item.as_string() {
147 | out.push(Token::new(&s, 0, 0, string_cache));
148 | } else if let Some(n) = item.as_f64() {
149 | out.push(Token::new_integer(n as i32, 0, 0));
150 | } else {
151 | return None;
152 | }
153 | }
154 | Some(out.normalize())
155 | } else {
156 | None
157 | }
158 | }
159 | Err(_) => None,
160 | }
161 | };
162 | js_from_update_result(update(core, side_input), &core.rules)
163 | } else {
164 | js_from_update_result(
165 | self.throne_context.update(),
166 | &self.throne_context.core.rules,
167 | )
168 | }
169 | }
170 |
171 | pub fn get_state(&self) -> JsValue {
172 | let string_cache = &self.throne_context.string_cache;
173 | let js_phrases = self
174 | .throne_context
175 | .core
176 | .state
177 | .get_all()
178 | .iter()
179 | .map(|phrase| js_value_from_phrase(phrase, string_cache))
180 | .collect::();
181 | JsValue::from(js_phrases)
182 | }
183 |
184 | pub fn get_state_hashes(&self) -> JsValue {
185 | let js_hashes = self
186 | .throne_context
187 | .core
188 | .state
189 | .get_all()
190 | .iter()
191 | .map(|phrase| {
192 | let mut hasher = DefaultHasher::new();
193 | phrase.hash(&mut hasher);
194 | JsValue::from(hasher.finish().to_string())
195 | })
196 | .collect::();
197 | JsValue::from(js_hashes)
198 | }
199 |
200 | pub fn print(&self) {
201 | log(&format!("{}", self.throne_context));
202 | }
203 | }
204 |
205 | fn js_value_from_phrase(phrase: &Phrase, string_cache: &StringCache) -> JsValue {
206 | let mut result = vec![];
207 | for group in phrase.groups() {
208 | if group.len() == 1 {
209 | result.push(js_value_from_atom(group[0].atom, string_cache));
210 | } else {
211 | result.push(js_value_from_phrase(&group.normalize(), string_cache));
212 | }
213 | }
214 |
215 | JsValue::from(result.iter().collect::())
216 | }
217 |
218 | fn js_value_from_atom(atom: Atom, string_cache: &StringCache) -> JsValue {
219 | if let Some(string) = string_cache.atom_to_str(atom) {
220 | JsValue::from(string)
221 | } else if let Some(n) = StringCache::atom_to_integer(atom) {
222 | JsValue::from(n)
223 | } else {
224 | JsValue::null()
225 | }
226 | }
227 |
228 | #[cfg(test)]
229 | mod tests {
230 | use wasm_bindgen_test::wasm_bindgen_test;
231 |
232 | use super::*;
233 | use crate::token::tokenize;
234 |
235 | #[wasm_bindgen_test]
236 | fn test_js_value_from_phrase_nested() {
237 | let mut string_cache = StringCache::new();
238 | let phrase = tokenize("t1 (t21 (t221 t222 t223) t23) t3", &mut string_cache);
239 | let js_phrase = js_value_from_phrase(&phrase, &string_cache);
240 | assert_eq!(
241 | format!("{:?}", js_phrase),
242 | r#"JsValue(["t1", ["t21", ["t221", "t222", "t223"], "t23"], "t3"])"#
243 | );
244 | }
245 |
246 | #[wasm_bindgen_test]
247 | fn test_js_value_from_phrase_nested2() {
248 | let mut string_cache = StringCache::new();
249 | let phrase = tokenize("t1 (t21 (t221 t222 t223))", &mut string_cache);
250 | log(&format!("{:#?}", phrase));
251 | let js_phrase = js_value_from_phrase(&phrase, &string_cache);
252 | assert_eq!(
253 | format!("{:?}", js_phrase),
254 | r#"JsValue(["t1", ["t21", ["t221", "t222", "t223"]]])"#
255 | );
256 | }
257 | }
258 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | [](https://crates.io/crates/throne)
4 | [](https://docs.rs/throne)
5 |
6 | A scripting language for game prototyping and story logic:
7 |
8 | ```
9 | // Declare the initial state as 'phrases', with one phrase per line.
10 | Mary is sister of David
11 | Sarah is child of Mary
12 | Tom is child of David
13 |
14 | // Define rules with the format: INPUT = OUTPUT.
15 | CHILD is child of PARENT . AUNT is sister of PARENT .
16 | COUSIN is child of AUNT = COUSIN is cousin of CHILD
17 |
18 | // The final state will be:
19 | // Sarah is cousin of Tom
20 | ```
21 |
22 | ## Motivation
23 |
24 | The original inspiration for Throne comes from languages used to author interactive fiction, such as [Inform](http://inform7.com/).
25 | As described in [this](https://brunodias.dev/2017/05/05/inform-prototyping.html) article, Inform can be used to prototype games from genres besides interactive fiction. By defining gameplay through rules, some of the verbosity of general purpose programming languages can be avoided. However, Inform and other interactive fiction authoring systems are too slow to execute their rules in every frame of a real-time gameplay loop and are difficult to embed in an existing engine.
26 |
27 | Throne allows gameplay logic to be defined through rules and so provides some of the benefits of a rule-based language like Inform, but is also fast to execute and easy to embed in an existing engine. Its rule syntax and mechanics began as simplified versions of those found in the [Ceptre](https://www.cs.cmu.edu/~cmartens/ceptre.pdf) programming language, which was the main influence for the design of Throne.
28 |
29 | ## Examples
30 | - [throne-playground](https://github.com/t-mw/throne-playground) - a web-based editor for Throne, made possible by compiling Throne to WebAssembly.
31 | - [blocks](examples/blocks.throne) - a simple tile matching game run with `cargo run --example blocks`.
32 | - [storylets-rs](https://github.com/t-mw/storylets-rs) - A storylet-based narrative engine for games.
33 |
34 | ## Reference
35 |
36 | Rules are of the format `INPUT = OUTPUT`, where `INPUT` and `OUTPUT` are lists that use period (`.`) as a separator between items:
37 | - `INPUT` is a list of one or more conditions that must pass for the rule to be executed. The conditions can either be state [phrases](#phrases) that must exist or [predicates](#predicates) that must evaluate to true. Any [matching](#matching) state phrases are consumed by the rule on execution.
38 | - `OUTPUT` is a list of state phrases that will be generated by the rule if it is executed.
39 | - Identifiers in rules that use only uppercase letters (`CHILD`, `PARENT`, `AUNT` and `COUSIN` in the snippet above) are variables that will be assigned when the rule is executed.
40 |
41 | Evaluating a Throne script involves executing any rule that matches the current state until the set of matching rules is exhausted. Rules are executed in a random order and may be executed more than once.
42 |
43 | ### Phrases
44 |
45 | The building blocks of a Throne script are phrases, which are collections of arbitrary character sequences (referred to in this section as *atoms*) separated by spaces. In other words, a phrase is a list of atoms separated by spaces.
46 |
47 | A list of phrases completely defines the state of a Throne script. Combined with [predicates](#predicates), lists of phrases also completely define the inputs and outputs of the rules in a Throne script.
48 |
49 | The following are examples of valid phrases:
50 |
51 | | Example | Notes |
52 | | --- | --- |
53 | | `1 2-abC Def_3` | ASCII alphanumeric characters, optionally mixed with dashes and underscores, are the simplest way to define a phrase. This phrase contains three atoms. |
54 | | `"!complex$" "1 2"` | Atoms containing non-ASCII alphanumeric characters, or that should include whitespace, must be double-quoted. This phrase contains two atoms. |
55 | | `foo MATCH_1ST AND-2ND` | Atoms that contain only uppercase ASCII alphanumeric characters, optionally mixed with dashes and underscores, define a variable when included in a rule's list of inputs. This phrase contains three atoms, the last two of which are variables. |
56 | | this (is (a nested) phrase) | Surrounding a set of atoms with parentheses causes them to be nested within the phrase. This affects variable [matching](#matching). |
57 |
58 | ### Predicates
59 |
60 | The following predicates can be used as one of the items in a rule's list of inputs:
61 |
62 | | Syntax | Effect | Example |
63 | | --- | --- | --- |
64 | | `+ X Y Z` | Matches when the sum of `X` and `Y` equals `Z` | `+ HEALTH 10 HEALED` |
65 | | `- X Y Z` | Matches when the sum of `Y` and `Z` equals `X` | `- HEALTH 10 DAMAGED` |
66 | | `< X Y` | Matches when `X` is less than `Y` | `< MONEY 0` |
67 | | `> X Y` | Matches when `X` is greater than `Y` | `> MONEY 0` |
68 | | `<= X Y` | Matches when `X` is less than or equal to `Y` | `<= MONEY 0` |
69 | | `>= X Y` | Matches when `X` is greater than or equal to `Y` | `>= MONEY 0` |
70 | | `% X Y Z` | Matches when the modulo of `X` with `Y` equals `Z` | `% DEGREES 360 REM` |
71 | | `== X Y` | Matches when `X` equals `Y` | `== HEALTH 100` |
72 | | `!X` | Matches when `X` does not exist in the state | `!this does not exist` |
73 | | `^X` | Calls the host application and matches depending on the response | `^os-clock-hour 12` |
74 |
75 | When a predicate accepts two input variables, both variables must be assigned a value for the predicate to produce a match. A value is assigned either by writing a constant inline or by sharing a variable with another of the rule's inputs.
76 | When a predicate accepts three input variables and one of the variables remains unassigned, it will be assigned the expected value according to the effect of the predicate e.g. `A` will be assigned the value `8` in `+ 2 A 10`.
77 |
78 | ### Matching
79 |
80 | Before a rule is executed, each of its inputs must be successfully matched to a unique state phrase.
81 | An input matches a state phrase if they are equal after all variables have been assigned.
82 |
83 | The following are examples of potential matches:
84 |
85 | | Rule Inputs | State Phrases | Outcome |
86 | | --- | --- | --- |
87 | | `health H = ...` | `health 100` | A successful match if `H` is unassigned or was already set to `100` by another input in the rule. `H` is then assigned the value `100`. |
88 | | `health H . health H = ...` | `health 100` | A failed match because only one compatible state phrase exists. |
89 | | `health H . health H = ...` |
health 100 health 200
| A failed match because `H` cannot be set to both `100` and `200`. |
90 | | `health H1 . health H2 = ...` |
health 100 health 200
| A successful match if `H1` and `H2` are unassigned or already assigned compatible values by other inputs in the rule. `H1` is then assigned the value of `100` or `200`, and `H2` is assigned the remaining value. |
91 | | `health H1 . health H2 . + H1 10 H2 = ...` |
health 100 health 200
| A failed match because the predicate cannot be satisfied. |
92 | | `health H = ...` | `health ((min 20) (max 100))` | A successful match if `H` is unassigned or was already set to `(min 20) (max 100)` by another input in the rule. `H` is then assigned the phrase `(min 20) (max 100)`. |
93 | | `health (_ (max H)) = ...` | `health ((min 20) (max 100))` | A successful match if `H` is unassigned or was already set to `100` by another input in the rule. `H` is then assigned the value `100`. `_` is a wildcard that will match anything, in this case the phrase `min 20`. |
94 |
95 | ### Constructs
96 |
97 | Special syntax exists to make it easier to write complex rules, but in the end these constructs compile down to the simple form described in the introduction. The following table lists the available constructs:
98 |
99 | | Syntax | Effect | Example | Compiled Form |
100 | | --- | --- | --- | --- |
101 | | Input phrase prefixed with `$` | Copies the input phrase to the rule output. | `$foo = bar` | `foo = bar . foo` |
102 | | A set of rules surrounded by curly braces prefixed with `INPUT:` where `INPUT` is a list of phrases | Copies `INPUT` to each rule's inputs. |