sfpm-js
Version:
A lightweight, dependency-free, forward-chaining inference engine for managing complex state and logic in a declarative way.
168 lines (136 loc) • 7 kB
Markdown
*AI Generated README*
# **sfpm-js**
**sfpm-js** is a lightweight, dependency-free, forward-chaining inference engine written in modern JavaScript. It is designed for managing complex state and logic in a declarative way, making it ideal for game development (especially for AI and quest/dialog systems), interactive narratives, simulations, and other dynamic applications.
The core principle is simple: you define a set of Rules, provide the current state as a collection of Facts, and the engine finds and executes the most specific rule that matches the current situation.
## **Core Concepts**
The system is built around a few key ideas:
* **Facts:** Simple key-value pairs that represent the current state of your world or application (e.g., PlayerLocation: 'Dungeon', HasKey: false). The collection of all current facts is managed by a FactSource.
* **Criteria:** The building blocks of rules. A single criterion is a condition that checks a fact against a value (e.g., "is the PlayerLocation fact equal to 'Dungeon'?").
* **Rules:** A rule is a collection of Criteria and a payload (a function to execute). A rule is considered a match only if **all** of its criteria are met by the current set of facts.
* **Query:** An object that holds the current facts and provides a simple interface to trigger the rule-matching process.
* **Specificity-Based Matching:** When multiple rules match the current facts, the engine automatically selects the **most specific** one to execute. Specificity is determined by the number of criteria in a rule—more criteria means more specific. If multiple rules have the same highest specificity, one is chosen at random.
## **Installation**
You can install the package using npm or your favorite package manager like bun or yarn.
```bash
npm install sfpm-js
```
Or with Bun:
```bash
bun add sfpm-js
```
## **Usage & API**
Here’s a step-by-step guide to using sfpm-js.
### **1\. Import the necessary classes**
```js
import { Query } from './src/Query.js';
import { Rule } from './src/Rule.js';
import { Criteria, Operator } from './src/Criteria.js';
```
### **2\. Create a Query object with initial facts**
The Query object holds the state of your application. You can initialize it with a Map of facts.
```js
// The world state is a collection of facts.
const worldState = new Map([
['playerHealth', 80],
['playerLocation', 'Forest'],
['isRaining', true],
['hasMagicSword', false],
]);
// The Query object is our interface to the rule engine.
const query = new Query(worldState);
```
### **3\. Define your Rules**
Rules consist of criteria and a payload. The Operator enum provides different ways to compare fact values.
* Operator.Equal
* Operator.NotEqual
* Operator.GreaterThan
* Operator.LessThan
* Operator.GreaterThanOrEqual
* Operator.LessThanOrEqual
* Operator.Predicate (for custom logic via a function)
```js
// This array will hold all the logic of our application.
const gameRules = [];
// Rule 1: A general rule for being in the forest.
gameRules.push(new Rule(
[
new Criteria('playerLocation', 'Forest', Operator.Equal)
],
() => {
console.log("It's a bit dark in the forest.");
}
));
// Rule 2: A more specific rule for when it's raining in the forest.
// This rule will be chosen over Rule 1 if it's raining because it has more criteria.
gameRules.push(new Rule(
[
new Criteria('playerLocation', 'Forest', Operator.Equal),
new Criteria('isRaining', true, Operator.Equal)
],
() => {
console.log("The forest is dark and wet from the rain. You should find shelter.");
},
"Rainy Forest Rule" // Optional name for debugging
));
// Rule 3: A rule that uses a predicate for a custom check.
// This rule is even more specific.
gameRules.push(new Rule(
[
new Criteria('playerLocation', 'Forest', Operator.Equal),
new Criteria('isRaining', true, Operator.Equal),
new Criteria('playerHealth', (health) => health \< 50, Operator.Predicate)
],
() => {
console.log("You're cold, wet, and injured. Your chances don't look good.");
}
));
// Rule 4: A rule that modifies the world state.
gameRules.push(new Rule(
[
new Criteria('playerLocation', 'Forest', Operator.Equal),
new Criteria('hasMagicSword', false, Operator.Equal)
],
() => {
console.log("You find a glowing sword stuck in a stone! You pull it free.");
// The payload can modify the facts for subsequent matches.
query.add('hasMagicSword', true);
},
"Find Sword Rule",
10 // A higher priority can break ties among rules with the same specificity.
));
```
### **4\. Run the Matcher**
Call query.match() with your rules to find and execute the best matching rule.
```js
console.log("--- First Turn \---");
query.match(gameRules);
// Expected Output: "You find a glowing sword stuck in a stone\! You pull it free."
// Why? "Find Sword Rule" (2 criteria) and "Rainy Forest Rule" (2 criteria) both match.
// "Find Sword Rule" has a higher priority (10 vs 0), so it wins.
console.log("\\n--- Second Turn \---");
// The state has changed (\`hasMagicSword\` is now true). Let's match again.
query.match(gameRules);
// Expected Output: "The forest is dark and wet from the rain. You should find shelter."
// Why? "Find Sword Rule" no longer matches. "Rainy Forest Rule" (2 criteria) is now the most specific valid rule.
// Now let's change the health to trigger the most specific rule.
console.log("\\n--- Third Turn (Health is low) \---");
query.add('playerHealth', 40);
query.match(gameRules);
// Expected Output: "You're cold, wet, and injured. Your chances don't look good."
// Why? The rule with 3 criteria now matches and is the most specific.
```
## **Examples**
This repository includes more advanced examples in the /example directory:
* textAdventure.js: A complete, playable mini text adventure game that showcases how to structure game logic, locations, and item interactions using sfpm-js.
* dialog.js: A sophisticated dialog system built on top of the engine. It demonstrates how to create branching conversations and manage character memory with a declarative API.
## **Running Tests**
The project uses bun:test for testing. To run the test suite:
bun test
## **Benchmarking**
A benchmark suite using mitata is included to measure the performance of rule evaluation and matching under various conditions.
To run the benchmarks:
bun run ./bench/sfpm.bench.js
## **Contributing**
Contributions are welcome\! If you find a bug or have a feature request, please open an issue. If you'd like to contribute code, please fork the repository and submit a pull request.
## **License**
This project is licensed under the MIT License. See the [LICENSE](https://www.google.com/search?q=LICENSE) file for details.