A powerful, general-purpose library for modeling and solving Constraint Satisfaction Problems (CSPs) in Dart. Built with intelligent backtracking search, consistency-checking algorithms, and smart heuristics to efficiently solve complex logic puzzles.
This library offers three intuitive ways to define constraints: string expressions for natural syntax, a high-level Problem builder for fast development, and a manual CspProblem class for direct control over the underlying structure.
Note: This project is originally based on a port and enhancement of the excellent csp.js by Prajit Ramachandran, adapted for Dart's strong typing, async capabilities, and object-oriented structure. This also enables easy use with Flutter projects.
A CSP is a mathematical problem where you need to find values for variables that satisfy a set of constraints. Every CSP consists of three components:
Component | Description | Example (Sudoku) |
---|---|---|
Variables | The unknowns you need to solve for | The 81 empty squares |
Domains | Possible values each variable can take | Numbers 1-9 for each square |
Constraints | Rules that restrict variable assignments | No repeats in rows/columns/blocks |
- Sudoku: Variables are grid squares, domains are numbers 1-9, constraints prevent duplicates in rows/columns/blocks
- Map Coloring: Variables are regions, domains are colors, constraints prevent adjacent regions from having the same color
- N-Queens: Variables are queen positions, domains are board squares, constraints prevent queens from attacking each other
This solver goes beyond brute-force search with the following implemented algorithms:
- Backtracking Search: Intelligent depth-first search that backtracks when constraints are violated
- AC-3 Algorithm: Enforces arc consistency for binary constraints (two-variable rules)
- Generalized Arc Consistency (GAC): Handles n-ary constraints (multi-variable rules)
- Multiple Solution Finding: Stream-based API to find all possible solutions efficiently
- Minimum Remaining Values (MRV): Chooses the most constrained variable to assign next ("fail-first" principle)
- Least Constraining Value (LCV): Selects values that preserve the most options for other variables
- 20+ Pre-built Constraints: Common constraint patterns like
allDifferent()
,exactSum()
,ascending()
, etc. - Optimized Performance: Built-in constraints are faster than equivalent lambda functions
- Extension Methods: Fluent API methods like
addAllDifferent()
for cleaner code
- Natural Language Syntax: Write constraints as strings like
"A + B == 10"
or"A != B != C"
- Advanced Expression Support: Complex expressions like
"5 <= A + B <= 7"
and"A * B + C == 15"
- Variable Equations: Support for
"A + B == C"
where one variable equals an expression of others - Set Membership: Constraints like
"A in [1, 3, 5]"
for allowed value sets - Comprehensive Parser: Handles arithmetic, comparisons, ranges, and chained operations
- Modular Architecture: Clean separation of concerns across multiple focused modules
- Comprehensive Test Suite: Full test coverage with test cases covering all functionality
- Multiple APIs: Choose between string constraints, builder pattern, or manual construction
- Fluent Builder API: An intuitive Problem class to easily define your CSP
- Rich Examples: Complete demo showcasing all constraint types and problem-solving techniques
- Debugging Tools: Problem validation, summary printing, and step-by-step visualization
- Type Safety: Full Dart type system integration with proper error handling
- Async Support: Non-blocking solving with
Future
-based API
Add this package to your pubspec.yaml
:
dependencies:
dart_csp: ^2.0.0
Then run:
dart pub get
import 'package:dart_csp/dart_csp.dart';
Future<void> main() async {
final p = Problem();
// Variables: Australian states, Domain: Colors
p.addVariables(['WA', 'NT', 'SA', 'Q', 'NSW', 'V'], ['red', 'green', 'blue']);
// Constraints using string expressions (easiest way!)
p.addStringConstraints([
'WA != SA',
'NT != SA',
'Q != SA',
'NSW != SA',
'V != SA',
'WA != NT',
'NT != Q',
'Q != NSW',
'NSW != V'
]);
// Get first solution
final solution = await p.getSolution();
print(solution); // {WA: red, NT: blue, SA: green, ...}
// Or find all solutions
print('All possible colorings:');
await for (final solution in p.getSolutions()) {
print(solution);
}
}
The library includes a complete demo showcasing all features:
dart run example/demo.dart
This runs 11 different constraint satisfaction problems demonstrating:
- USA Map Coloring
- N-Queens Problem
- Sudoku Solving
- Magic Square Generation
- Resource Allocation
- Class Scheduling
- And more!
The most intuitive way - write constraints as natural expressions:
final p = Problem();
// 1. Add variables and domains
p.addVariables(['A', 'B', 'C'], [1, 2, 3, 4, 5]);
// 2. Add constraints using natural string syntax
p.addStringConstraints([
'A != B != C', // All different (chained)
'A + B == 7', // Exact sum
'A < B < C', // Strict ordering
'5 <= A + B <= 8', // Range constraint
'A * B >= 6', // Minimum product
'A + B == C' // Variable equation
]);
// 3. Solve
final solution = await p.getSolution();
Supported String Constraint Syntax:
Pattern | Description | Example |
---|---|---|
Equality/Inequality | Variable comparisons | "A == B" , "A != B" |
Chained Operations | Multiple comparisons | "A != B != C" , "A < B < C" |
Arithmetic Equality | Exact sums/products | "A + B == 10" , "A * B == 12" |
Arithmetic Inequality | Min/max constraints | "A + B >= 5" , "A * B <= 20" |
Range Constraints | Bounded values | "5 <= A + B <= 10" |
Variable Equations | Inter-variable relations | "A + B == C" , "A * B == D" |
Complex Expressions | Mixed operations | "2*A + 3*B == 15" , "A*B + C >= 10" |
Set Membership | Allowed values | "A in [1, 3, 5]" |
Single Variable | Constant comparisons | "A > 5" , "B != 3" |
The Problem class provides a clean, step-by-step builder pattern with built-in constraints:
final p = Problem();
// 1. Add variables and domains
p.addVariables(['A', 'B', 'C'], [1, 2, 3, 4, 5]);
// 2. Use built-in constraints (highly optimized)
p.addAllDifferent(['A', 'B', 'C']); // All different values
p.addExactSum(['A', 'B'], 7); // A + B = 7
p.addAscending(['A', 'B', 'C']); // A ≤ B ≤ C
// 3. Or define custom constraints with lambda functions
p.addConstraint(['A', 'B'], (a, b) => a * b <= 10);
// 4. Solve
final solution = await p.getSolution();
Direct access to underlying data structures for programmatic generation:
var variables = <String, List<dynamic>>{
'A': [1, 2, 3],
'B': [1, 2, 3, 4],
'C': [3, 4, 5],
};
var binaryConstraints = <BinaryConstraint>[
BinaryConstraint('A', 'B', (a, b) => a < b),
BinaryConstraint('B', 'A', (b, a) => b > a),
];
final problem = CspProblem(
variables: variables,
constraints: binaryConstraints,
);
final solution = await CSP.solve(problem);
The library provides optimized, reusable constraint functions for common patterns:
// All variables must have different values
p.addAllDifferent(['A', 'B', 'C', 'D']);
// All variables must have the same value
p.addAllEqual(['X', 'Y', 'Z']);
// Sum constraints
p.addExactSum(['A', 'B', 'C'], 15); // A + B + C = 15
p.addSumRange(['A', 'B'], 5, 10); // 5 ≤ A + B ≤ 10
// Weighted sums
p.addExactSum(['A', 'B'], 20, multipliers: [3, 4]); // 3*A + 4*B = 20
// Product constraints
p.addExactProduct(['X', 'Y'], 12); // X * Y = 12
p.addConstraint(['A', 'B', 'C'], minProduct(8)); // A * B * C ≥ 8
// Variables must be from allowed set
p.addInSet(['A', 'B'], {2, 3, 5, 7}); // Only prime numbers
// Variables cannot be from forbidden set
p.addNotInSet(['X', 'Y'], {2, 4, 6}); // No even numbers
// At least N variables must be from set
p.addConstraint(['A', 'B', 'C'], someInSet({1, 3, 5}, 2)); // ≥2 odd numbers
// Ordering (preserves variable sequence)
p.addAscending(['A', 'B', 'C']); // A ≤ B ≤ C
p.addStrictlyAscending(['X', 'Y', 'Z']); // X < Y < Z
p.addDescending(['P', 'Q', 'R']); // P ≥ Q ≥ R
You can also use the constraint factory functions directly:
// Using factory functions with addConstraint()
p.addConstraint(['A', 'B', 'C'], allDifferent());
p.addConstraint(['X', 'Y'], exactSumBinary(10)); // For 2 variables, more efficient
// Custom combinations
p.addConstraint(['A', 'B', 'C'], (assignment) {
// Custom logic combining multiple constraint types
return allDifferent()(assignment) && exactSum(12)(assignment);
});
For quick problem solving, use the top-level convenience functions:
// Quick all-different problem
final solution1 = await solveAllDifferent(
variables: ['A', 'B', 'C'],
domain: [1, 2, 3]
);
// Quick sum problem
final solution2 = await solveSumProblem(
variables: ['X', 'Y'],
domain: [1, 2, 3, 4, 5],
targetSum: 7
);
// General string constraint problem
final solution3 = await solveProblem(
variables: {'A': [1, 2, 3, 4], 'B': [1, 2, 3, 4]},
constraints: ['A != B', 'A + B >= 5']
);
Future<void> solveSudoku(List<List<int>> puzzle) async {
final p = Problem();
// Add variables for each cell
for (int r = 0; r < 9; r++) {
for (int c = 0; c < 9; c++) {
final key = '$r-$c';
if (puzzle[r][c] != 0) {
p.addVariable(key, [puzzle[r][c]]); // Clue
} else {
p.addVariable(key, [1, 2, 3, 4, 5, 6, 7, 8, 9]); // Empty cell
}
}
}
// Add all-different constraints using built-in methods
// Rows
for (int r = 0; r < 9; r++) {
final row = List.generate(9, (c) => '$r-$c');
p.addAllDifferent(row);
}
// Columns
for (int c = 0; c < 9; c++) {
final col = List.generate(9, (r) => '$r-$c');
p.addAllDifferent(col);
}
// 3x3 blocks
for (int br in [0, 3, 6]) {
for (int bc in [0, 3, 6]) {
final block = <String>[];
for (int r = br; r < br + 3; r++) {
for (int c = bc; c < bc + 3; c++) {
block.add('$r-$c');
}
}
p.addAllDifferent(block);
}
}
final solution = await p.getSolution();
// Print solved puzzle...
}
Future<void> generateMagicSquare() async {
final p = Problem();
// 3x3 grid with numbers 1-9
final positions = ['A1', 'A2', 'A3', 'B1', 'B2', 'B3', 'C1', 'C2', 'C3'];
p.addVariables(positions, [1, 2, 3, 4, 5, 6, 7, 8, 9]);
// Each number appears exactly once
p.addAllDifferent(positions);
// All rows, columns, and diagonals sum to 15 using string constraints
p.addStringConstraints([
// Rows
'A1 + A2 + A3 == 15',
'B1 + B2 + B3 == 15',
'C1 + C2 + C3 == 15',
// Columns
'A1 + B1 + C1 == 15',
'A2 + B2 + C2 == 15',
'A3 + B3 + C3 == 15',
// Diagonals
'A1 + B2 + C3 == 15',
'A3 + B2 + C1 == 15'
]);
final solution = await p.getSolution();
// Display magic square...
}
Future<void> allocateResources() async {
final p = Problem();
// Teams get 3-10 resources each
p.addVariables(['TeamA', 'TeamB', 'TeamC'], [3, 4, 5, 6, 7, 8, 9, 10]);
// Use string constraints for clarity
p.addStringConstraints([
'TeamA + TeamB + TeamC == 20', // Total budget
'TeamA >= TeamB', // Priority constraint
'TeamA >= 3', // Minimum allocation
'TeamB >= 3',
'TeamC >= 3'
]);
final solution = await p.getSolution();
print('Team A: ${solution['TeamA']} resources');
print('Team B: ${solution['TeamB']} resources');
print('Team C: ${solution['TeamC']} resources');
}
The library includes a comprehensive test suite covering all functionality:
# Run all tests
dart test
# Run tests with coverage
dart pub global activate coverage
dart pub global run coverage:test_with_coverage
# Run specific test groups
dart test test/dart_csp_test.dart -n "Basic Problem Creation"
dart test test/dart_csp_test.dart -n "String Constraints"
The test suite includes test cases covering:
- Basic Problem Creation: Variable addition, domain validation, constraint setup
- Binary Constraints: Two-variable relationships and consistency
- N-ary Constraints: Multi-variable constraints and complex relationships
- String Constraints: All parsing scenarios and edge cases
- Built-in Constraints: Every constraint factory function and extension method
- Complex Problems: Magic squares, N-Queens, Sudoku, map coloring
- Convenience Functions: Top-level utility functions
- Failure Cases: Over-constrained and unsolvable problems
- Problem Utilities: Validation, debugging, and introspection
- Edge Cases: Malformed constraints, missing variables, empty domains
final p = Problem();
p.addVariables(['A', 'B', 'C'], [1, 2, 3]);
p.addStringConstraint('A != B');
// Print problem summary
p.printSummary();
// Validate problem for common issues
final issues = p.validate();
if (issues.isEmpty) {
print('Problem validation: ✓ No issues found');
} else {
print('Problem validation issues:');
for (final issue in issues) {
print(' - $issue');
}
}
Monitor the solver's progress with callback functions:
void visualizer(
Map<String, List<dynamic>> assigned,
Map<String, List<dynamic>> unassigned,
) {
print("\n--- Solver Step ---");
print("Assigned: $assigned");
print("Unassigned Domains: $unassigned");
}
final p = Problem();
// ... define problem ...
p.setOptions(
timeStep: 100, // 100ms pause between steps
callback: visualizer,
);
final solution = await p.getSolution();
- Use String Constraints: Often more readable and just as fast as built-ins
- Use Built-in Constraints:
addAllDifferent()
is faster than custom lambdas - Restrict Domains Early: Smaller initial domains = faster solving
- Strategic Constraint Ordering: Add most constraining constraints first
- Consider Clues: For puzzles, pre-fill strategic positions to reduce search space
// Performance example: Magic square with strategic clue
final p = Problem();
// Add strategic clue to dramatically reduce search space
p.addVariable('B2', [5]); // Center is always 5 in 3x3 magic square
// Remaining variables exclude the clue value
final otherPositions = ['A1', 'A2', 'A3', 'B1', 'B3', 'C1', 'C2', 'C3'];
p.addVariables(otherPositions, [1, 2, 3, 4, 6, 7, 8, 9]); // No 5
// ... add constraints using string syntax ...
p.addStringConstraints([
'A1 + A2 + A3 == 15',
'B1 + B2 + B3 == 15', // B2 is constrained to [5]
// ... more constraints
]);
Method | Parameters | Description |
---|---|---|
addVariable() |
String name , List<dynamic> domain |
Adds a single variable with its domain |
addVariables() |
List<String> names , List<dynamic> domain |
Adds multiple variables sharing the same domain |
addConstraint() |
List<String> vars , Function predicate |
Adds custom binary or n-ary constraint |
addStringConstraint() |
String constraint |
Adds constraint from string expression |
addStringConstraints() |
List<String> constraints |
Adds multiple string constraints |
setOptions() |
int? timeStep , CspCallback? callback |
Sets visualization parameters |
getSolution() |
(none) | Builds and solves the problem |
Method | Parameters | Description |
---|---|---|
addAllDifferent() |
List<String> variables |
All variables have different values |
addAllEqual() |
List<String> variables |
All variables have the same value |
addExactSum() |
List<String> vars , num sum , List<num>? multipliers |
Variables sum to exact value |
addSumRange() |
List<String> vars , num min , num max , List<num>? multipliers |
Sum within range |
addExactProduct() |
List<String> vars , num product |
Variables multiply to exact value |
addInSet() |
List<String> vars , Set<dynamic> allowed |
Variables must be from allowed set |
addNotInSet() |
List<String> vars , Set<dynamic> forbidden |
Variables cannot be from forbidden set |
addAscending() |
List<String> variables |
Variables in non-decreasing order |
addStrictlyAscending() |
List<String> variables |
Variables in strictly increasing order |
addDescending() |
List<String> variables |
Variables in non-increasing order |
Method | Returns | Description |
---|---|---|
printSummary() |
void |
Prints problem overview to console |
validate() |
List<String> |
Returns list of potential issues |
copy() |
Problem |
Creates a deep copy of the problem |
clear() |
void |
Removes all variables and constraints |
variableCount |
int |
Number of variables in problem |
constraintCount |
int |
Number of constraints in problem |
allDifferent()
→NaryPredicate
allDifferentBinary()
→BinaryPredicate
allEqual()
→NaryPredicate
allEqualBinary()
→BinaryPredicate
exactSum(num target, {List<num>? multipliers})
→NaryPredicate
minSum(num minimum, {List<num>? multipliers})
→NaryPredicate
maxSum(num maximum, {List<num>? multipliers})
→NaryPredicate
sumInRange(num min, num max, {List<num>? multipliers})
→NaryPredicate
exactProduct(num target)
→NaryPredicate
minProduct(num minimum)
→NaryPredicate
maxProduct(num maximum)
→NaryPredicate
inSet(Set<dynamic> allowed)
→NaryPredicate
notInSet(Set<dynamic> forbidden)
→NaryPredicate
someInSet(Set<dynamic> values, int minimum)
→NaryPredicate
someNotInSet(Set<dynamic> values, int minimum)
→NaryPredicate
ascendingInOrder(List<String> order)
→NaryPredicate
strictlyAscendingInOrder(List<String> order)
→NaryPredicate
descendingInOrder(List<String> order)
→NaryPredicate
Note: Binary versions (suffix Binary
) are available for 2-variable optimizations.
Function | Parameters | Description |
---|---|---|
solveProblem() |
Map<String, List<dynamic>> variables , List<String> constraints |
Solve with string constraints |
solveAllDifferent() |
List<String> variables , List<dynamic> domain |
Quick all-different solver |
solveSumProblem() |
List<String> variables , List<dynamic> domain , num targetSum |
Quick sum constraint solver |
Property | Type | Required | Description |
---|---|---|---|
variables |
Map<String, List<dynamic>> |
✅ | Variable names mapped to their domains |
constraints |
List<BinaryConstraint> |
❌ | Rules between pairs of variables |
naryConstraints |
List<NaryConstraint> |
❌ | Rules among groups of variables |
cb |
CspCallback? |
❌ | Visualization callback function |
timeStep |
int |
❌ | Delay in ms between visualization steps |
BinaryConstraint(
String head, // First variable
String tail, // Second variable
bool Function(dynamic, dynamic) predicate // Validation function
)
NaryConstraint({
required List<String> vars, // All involved variables
required bool Function(Map<String, dynamic>) predicate // Validation function
})
typedef BinaryPredicate = bool Function(dynamic headVal, dynamic tailVal);
typedef NaryPredicate = bool Function(Map<String, dynamic> assignment);
typedef CspCallback = void Function(
Map<String, List<dynamic>> assigned,
Map<String, List<dynamic>> unassigned
);
The library is organized into focused, modular components:
lib/
├── dart_csp.dart # Main library export and convenience functions
└── src/
├── types.dart # Core type definitions and interfaces
├── solver.dart # CSP solver with backtracking, AC-3, GAC
├── problem.dart # Problem builder class and extensions
├── builtin_constraints.dart # Optimized constraint factory functions
└── constraint_parser.dart # String constraint parsing engine
example/
├── demo.dart # Comprehensive demo of all features
├── usage_examples.dart # Usage examples for the APIs
├── gencw.dart # Advanced arithmetic crosswords puzzle generator
└── gensq.dart # Advanced arithmetic square puzzle generator
test/
└── dart_csp_test.dart # Comprehensive test suite
The comprehensive demo (example/demo.dart
) showcases all library features with 11 different problems:
dart run example/demo.dart
- USA Map Coloring - Compares old vs new constraint methods
- 8-Queens Problem - Classic backtracking demonstration
- Sudoku Solver - Using built-in
allDifferent
constraints - All Different & Equal Constraints - Basic constraint examples
- Sum Constraints - Arithmetic constraint varieties
- Product Constraints - Multiplication-based rules
- Set Membership - Value inclusion/exclusion constraints
- Ordering Constraints - Sequential arrangement rules
- Magic Square - Complex multi-constraint problem with random clues
- Resource Allocation - Real-world optimization scenario
- Class Scheduling - Timetabling with multiple constraint types
Each demo includes:
- Problem setup explanation
- Constraint definition examples
- Solution verification
- Performance timing
- Use String Constraints: Natural syntax with good performance
- Use Built-in Constraints: Pre-optimized functions are faster than equivalent lambdas
- Strategic Domain Reduction: Limit initial domains as much as possible
- Constraint Ordering: Add most restrictive constraints first
- Choose Appropriate Constraint Types: Use binary constraints for 2-variable rules
- Consider Problem Structure: Add strategic "clues" to reduce search space
// Readable and fast: String constraints
p.addStringConstraints(['A != B != C', 'A + B + C == 10']);
// Also fast: Built-in constraints
p.addAllDifferent(['A', 'B', 'C']);
p.addExactSum(['A', 'B', 'C'], 10);
// Slower but flexible: Custom lambda
p.addConstraint(['A', 'B', 'C'], (assignment) {
final values = assignment.values.toSet();
return values.length == assignment.length &&
values.fold<num>(0, (sum, v) => sum + v) == 10;
});
// Fastest for 2 variables: Binary constraint
p.addConstraint(['A', 'B'], allDifferentBinary());
Contributions are welcome! This library builds upon the excellent foundation of csp.js by Prajit Ramachandran.
- Clone the repository
- Install dependencies:
dart pub get
- Run tests:
dart test
- Run the demo:
dart run example/demo.dart
- Run examples:
dart run example/usage_examples.dart
MIT License - see LICENSE file for details.
Built with ❤️ for the Dart & Flutter community