├── lib ├── csp │ ├── version.rb │ ├── utils.rb │ ├── algorithms │ │ ├── lookahead │ │ │ ├── no_algorithm.rb │ │ │ └── ac3.rb │ │ ├── ordering │ │ │ └── no_order.rb │ │ ├── filtering │ │ │ └── no_filter.rb │ │ └── backtracking.rb │ ├── constraint.rb │ ├── constraints.rb │ └── problem.rb └── csp-resolver.rb ├── .gitignore ├── bin ├── setup └── console ├── Gemfile ├── Rakefile ├── spec ├── examples │ └── sculpture │ │ ├── sculpture_spec.rb │ │ ├── event_scheduling_spec.rb │ │ ├── queen_spec.rb │ │ ├── map_coloring_spec.rb │ │ └── constraints │ │ ├── only_one_constraint_spec.rb │ │ ├── queens_constraint_spec.rb │ │ ├── map_coloring_constraint_spec.rb │ │ ├── room_limit_to_one_constraint_spec.rb │ │ ├── cannot_be_in_same_room_constraint_spec.rb │ │ └── must_be_in_same_room_constraint_spec.rb ├── csp │ ├── constraint_spec.rb │ ├── algorithms │ │ ├── ordering │ │ │ └── no_order_spec.rb │ │ ├── filtering │ │ │ └── no_filter_spec.rb │ │ ├── lookahead │ │ │ └── ac3_spec.rb │ │ └── backtracking_spec.rb │ ├── constraints_spec.rb │ └── problem_spec.rb ├── shared │ └── algorithms │ │ └── filtering_ordering.rb ├── support │ └── tshirt.rb └── spec_helper.rb ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── workflows │ ├── release.yml │ └── main.yml └── pull_request_template.md ├── .rubocop.yml ├── MIT-LICENSE ├── examples ├── queen.rb ├── event_scheduling.rb ├── sculpture.rb └── map_coloring.rb ├── csp-resolver.gemspec ├── Gemfile.lock ├── CONTRIBUTING.md ├── CODE_OF_CONDUCT.md └── README.md /lib/csp/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CSP 4 | VERSION = '0.1.0' 5 | end 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | /vendor/ 10 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | gemspec 6 | 7 | group :development do 8 | gem 'rake', '~> 13.0' 9 | gem 'rspec' 10 | gem 'rubocop', '~> 1.28', require: false 11 | gem 'simplecov', require: false 12 | end 13 | -------------------------------------------------------------------------------- /lib/csp/utils.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Utils 4 | module Array 5 | def self.wrap(object) 6 | if object.nil? 7 | [] 8 | elsif object.respond_to?(:to_ary) 9 | object.to_ary || [object] 10 | else 11 | [object] 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'bundler/setup' 5 | require 'csp' 6 | 7 | # You can add fixtures and/or initialization code here to make experimenting 8 | # with your gem easier. You can also use a different console, if you like. 9 | 10 | require 'irb' 11 | IRB.start(__FILE__) 12 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/gem_tasks' 4 | require 'rspec/core/rake_task' 5 | require 'rubocop/rake_task' 6 | 7 | RSpec::Core::RakeTask.new(:spec) do |t| 8 | t.pattern = 'spec/**/*_spec.rb' 9 | t.verbose = false 10 | end 11 | 12 | RuboCop::RakeTask.new(:rubocop) do |t| 13 | t.options = ['--format', 'simple'] 14 | end 15 | 16 | task default: :spec 17 | -------------------------------------------------------------------------------- /spec/examples/sculpture/sculpture_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | require_relative '../../../examples/sculpture' 5 | 6 | RSpec.describe CSP::Examples::Sculpture do 7 | describe '#call' do 8 | it 'returns the solution for the problem' do 9 | expect(described_class.new.call).to eq([{ 'A' => 2, 'B' => 1, 'C' => 1 }]) 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/examples/sculpture/event_scheduling_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | require_relative '../../../examples/event_scheduling' 5 | 6 | RSpec.describe CSP::Examples::EventScheduling do 7 | describe '#call' do 8 | it 'returns the solution for the problem' do 9 | expect(described_class.new.call).to eq([{ 0 => 0, 1 => 1, 2 => 2 }]) 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/examples/sculpture/queen_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | require_relative '../../../examples/queen' 5 | 6 | RSpec.describe CSP::Examples::Queen do 7 | describe '#call' do 8 | it 'returns the solution for the problem' do 9 | expect(described_class.new.call).to eq([{ 0 => 0, 1 => 4, 2 => 7, 3 => 5, 4 => 2, 5 => 6, 6 => 1, 7 => 3 }]) 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/csp/constraint_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe CSP::Constraint do 6 | describe '#satisfies?' do 7 | context 'is not implemented yet' do 8 | it 'raises a NotImplementedError' do 9 | constraint = CSP::Constraint.new 10 | expect { constraint.satisfies? }.to raise_error(StandardError, 'Not Implemented. Should return a boolean') 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/csp/algorithms/lookahead/no_algorithm.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CSP 4 | module Algorithms 5 | module Lookahead 6 | class NoAlgorithm 7 | attr_reader :problem 8 | 9 | def initialize(problem) 10 | @problem = problem 11 | end 12 | 13 | def call(variables:, assignment:, domains:) # rubocop:disable Lint/UnusedMethodArgument 14 | domains 15 | end 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/csp-resolver.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'csp/version' 4 | require_relative 'csp/constraint' 5 | require_relative 'csp/utils' 6 | require_relative 'csp/algorithms/filtering/no_filter' 7 | require_relative 'csp/algorithms/ordering/no_order' 8 | require_relative 'csp/algorithms/lookahead/no_algorithm' 9 | require_relative 'csp/algorithms/lookahead/ac3' 10 | require_relative 'csp/algorithms/backtracking' 11 | require_relative 'csp/constraints' 12 | require_relative 'csp/problem' 13 | 14 | module CSP; end 15 | -------------------------------------------------------------------------------- /lib/csp/algorithms/ordering/no_order.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CSP 4 | module Algorithms 5 | module Ordering 6 | class NoOrder 7 | attr_reader :problem 8 | 9 | def self.for(problem:, dependency: nil) # rubocop:disable Lint/UnusedMethodArgument 10 | new(problem) 11 | end 12 | 13 | def initialize(problem) 14 | @problem = problem 15 | end 16 | 17 | def call(variables) 18 | variables 19 | end 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/csp/constraint.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CSP 4 | class Constraint 5 | attr_reader :variables 6 | 7 | def initialize(variables = []) 8 | @variables = variables 9 | end 10 | 11 | def satisfies?(_assignment = {}) 12 | raise StandardError, 'Not Implemented. Should return a boolean' 13 | end 14 | 15 | def unary? 16 | arity == 1 17 | end 18 | 19 | def binary? 20 | arity == 2 21 | end 22 | 23 | def arity 24 | @arity ||= variables.size 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## **Description** 11 | A clear and concise description of what the bug is. 12 | 13 | ## **Steps to Reproduce** 14 | Steps to reproduce the behavior. 15 | 16 | ## **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | ## **Actual behavior** 20 | A clear and concise description of what really happens. 21 | 22 | ## **Additional context** 23 | Add any other context about the problem here. 24 | -------------------------------------------------------------------------------- /spec/csp/algorithms/ordering/no_order_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | require_relative '../../../shared/algorithms/filtering_ordering' 5 | 6 | RSpec.describe CSP::Algorithms::Ordering::NoOrder do 7 | it_behaves_like 'filter or ordering algorithm initializes with problem' 8 | 9 | describe '#call' do 10 | it 'returns the values in same order' do 11 | problem = spy 12 | variables = [1, 2, 3] 13 | 14 | order_algorithm = described_class.new(problem) 15 | 16 | expect(order_algorithm.call(variables)).to eq [1, 2, 3] 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/csp/algorithms/filtering/no_filter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CSP 4 | module Algorithms 5 | module Filtering 6 | class NoFilter 7 | attr_reader :problem 8 | 9 | def self.for(problem:, dependency: nil) # rubocop:disable Lint/UnusedMethodArgument 10 | new(problem) 11 | end 12 | 13 | def initialize(problem) 14 | @problem = problem 15 | end 16 | 17 | def call(values:, assignment_values: []) # rubocop:disable Lint/UnusedMethodArgument 18 | values 19 | end 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/csp/algorithms/filtering/no_filter_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | require_relative '../../../shared/algorithms/filtering_ordering' 5 | 6 | RSpec.describe CSP::Algorithms::Filtering::NoFilter do 7 | it_behaves_like 'filter or ordering algorithm initializes with problem' 8 | 9 | describe '#call' do 10 | it 'returns the values without any change' do 11 | problem = spy 12 | values = [1, 2, 3] 13 | 14 | filter_algorithm = described_class.new(problem) 15 | 16 | filtered_variables = filter_algorithm.call(values: values, assignment_values: []) 17 | 18 | expect(filtered_variables).to eq [1, 2, 3] 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Description 11 | **Is your feature request related to a problem? Please describe.** 12 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 13 | 14 | ## Proposed Solution 15 | **Describe the solution you'd like** 16 | A clear and concise description of what you want to happen. 17 | 18 | **Describe alternatives you've considered** 19 | A clear and concise description of any alternative solutions or features you've considered. 20 | 21 | ## Additional context 22 | Add any other context or screenshots about the feature request here. 23 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Gem Release 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | build: 8 | name: Publish Gem 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - name: Set up Ruby 13 | uses: ruby/setup-ruby@v1 14 | with: 15 | ruby-version: '2.5.8' 16 | - name: Publish to RubyGems 17 | run: | 18 | mkdir -p $HOME/.gem 19 | touch $HOME/.gem/credentials 20 | chmod 0600 $HOME/.gem/credentials 21 | printf -- "---\n:rubygems: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials 22 | gem build *.gemspec 23 | gem push --key rubygems *.gem 24 | env: 25 | GEM_HOST_API_KEY: "${{secrets.RUBYGEMS_AUTH_TOKEN}}" 26 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Ruby CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | 9 | test: 10 | name: Run tests 11 | runs-on: ubuntu-latest 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | ruby-version: ['2.5.8', '2.6', '2.7', '3.0', '3.1', '3.2.2'] 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Set up Ruby 19 | uses: ruby/setup-ruby@v1 20 | with: 21 | ruby-version: ${{ matrix.ruby-version }} 22 | bundler-cache: true 23 | - name: Install dependencies 24 | run: bundle install --jobs 4 25 | - name: Run RuboCop 26 | run: bundle exec rubocop --config .rubocop.yml 27 | - name: Run RSpec + SimpleCov 28 | run: COVERAGE=true bundle exec rspec --format documentation -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | TargetRubyVersion: 2.5 3 | NewCops: enable 4 | SuggestExtensions: false 5 | Metrics/BlockLength: 6 | Exclude: 7 | - spec/**/*.rb 8 | Metrics/ClassLength: 9 | Max: 200 10 | Metrics/MethodLength: 11 | Max: 20 12 | Layout/FirstArrayElementIndentation: 13 | EnforcedStyle: consistent 14 | Layout/FirstHashElementIndentation: 15 | EnforcedStyle: consistent 16 | Layout/HashAlignment: 17 | EnforcedLastArgumentHashStyle: always_inspect 18 | Layout/MultilineMethodCallIndentation: 19 | EnforcedStyle: indented 20 | Style/Documentation: 21 | Enabled: false 22 | Layout/EndAlignment: 23 | EnforcedStyleAlignWith: variable 24 | Style/ObjectThen: 25 | EnforcedStyle: yield_self 26 | Naming/FileName: 27 | Exclude: 28 | - 'lib/csp-resolver.rb' 29 | Gemspec/RequireMFA: 30 | Enabled: false 31 | -------------------------------------------------------------------------------- /spec/examples/sculpture/map_coloring_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | require_relative '../../../examples/map_coloring' 5 | 6 | RSpec.describe CSP::Examples::MapColoring do 7 | describe '#call' do 8 | it 'returns the solution for the problem' do 9 | expect(described_class.new.call).to eq([{ 'Western Australia' => 'red', 10 | 'Northern Territory' => 'blue', 11 | 'South Australia' => 'green', 12 | 'Queensland' => 'red', 13 | 'New South Wales' => 'blue', 14 | 'Victoria' => 'red', 15 | 'Tasmania' => 'blue' }]) 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/shared/algorithms/filtering_ordering.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.shared_examples 'filter or ordering algorithm initializes with problem' do 6 | describe '.for' do 7 | context 'when receives a problem (csp) and dependency' do 8 | it ' initialize using problem' do 9 | problem = spy 10 | dependency = spy 11 | 12 | order_or_filter = described_class.for(problem: problem, dependency: dependency) 13 | 14 | expect(order_or_filter).to have_attributes(class: described_class, problem: problem) 15 | end 16 | end 17 | 18 | context 'when no dependency is received' do 19 | it 'initialize using problem' do 20 | problem = spy 21 | 22 | order_or_filter = described_class.for(problem: problem) 23 | 24 | expect(order_or_filter).to have_attributes(class: described_class, problem: problem) 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2024 Rebase, André Benjamim, Gustavo Alberto. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /lib/csp/constraints.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CSP 4 | module Constraints 5 | class AllDifferentConstraint < CSP::Constraint 6 | def satisfies?(assignment) 7 | assignment.values == assignment.values.uniq 8 | end 9 | end 10 | 11 | class UniqueConstraint < CSP::Constraint 12 | def satisfies?(assignment) 13 | values = assignment.values_at(*variables) 14 | 15 | return true if values.any?(&:nil?) 16 | 17 | values == values.uniq 18 | end 19 | end 20 | 21 | class CustomConstraint < CSP::Constraint 22 | attr_reader :block 23 | 24 | def initialize(variables, block) 25 | super(variables) 26 | @block = block 27 | end 28 | 29 | def satisfies?(assignment) 30 | values = assignment.values_at(*variables) 31 | return true if values.any?(&:nil?) 32 | 33 | block.call(*values) 34 | end 35 | end 36 | 37 | def all_different 38 | add_constraint(AllDifferentConstraint.new(variables)) 39 | 40 | self 41 | end 42 | 43 | def unique(variables) 44 | add_constraint(UniqueConstraint.new(variables)) 45 | 46 | self 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /examples/queen.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../lib/csp-resolver' 4 | 5 | module CSP 6 | module Examples 7 | class Queen 8 | def call(queens_number = 8) 9 | variables = queens_number.times.to_a 10 | 11 | csp = CSP::Problem.new 12 | .add_variables(variables, domains: variables) 13 | .add_constraint(QueensConstraint.new(variables)) 14 | solution = csp.solve 15 | 16 | solution || 'No solution found' 17 | end 18 | 19 | class QueensConstraint < ::CSP::Constraint 20 | attr_reader :columns 21 | 22 | def initialize(columns) 23 | super(columns) 24 | 25 | @columns = columns 26 | end 27 | 28 | def satisfies?(assignment) 29 | assignment.each do |(queen_col1, queen_row1)| 30 | (queen_col1 + 1..columns.size).each do |queen_col2| 31 | next unless assignment.key?(queen_col2) 32 | 33 | queen_row2 = assignment[queen_col2] 34 | 35 | return false if queen_row1 == queen_row2 36 | return false if (queen_row1 - queen_row2).abs == (queen_col1 - queen_col2).abs 37 | end 38 | end 39 | 40 | true 41 | end 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /examples/event_scheduling.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../lib/csp-resolver' 4 | 5 | module CSP 6 | module Examples 7 | class EventScheduling 8 | def call 9 | number_of_events = 3 10 | number_of_time_slots = 4 11 | 12 | variables = number_of_events.times.to_a 13 | domains = number_of_time_slots.times.to_a 14 | 15 | csp = CSP::Problem.new 16 | .add_variables(variables, domains: domains) 17 | 18 | variables.combination(2).each do |events| 19 | add_constraint(csp, *events) 20 | end 21 | 22 | solution = csp.solve 23 | solution || 'No solution found' 24 | end 25 | 26 | def add_constraint(csp, event1, event2) 27 | csp.add_constraint(OnlyOneConstraint.new(event1, event2)) 28 | end 29 | 30 | class OnlyOneConstraint < ::CSP::Constraint 31 | attr_reader :event1, :event2 32 | 33 | def initialize(event1, event2) 34 | super([event1, event2]) 35 | 36 | @event1 = event1 37 | @event2 = event2 38 | end 39 | 40 | def satisfies?(assignment) 41 | return true if variables.any? { |variable| !assignment.key?(variable) } 42 | 43 | assignment[event1] != assignment[event2] 44 | end 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /spec/support/tshirt.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Tshirt 4 | def people 5 | %w[A B C] 6 | end 7 | 8 | def colors 9 | %w[red green blue] 10 | end 11 | 12 | class UniqueConstraint < ::CSP::Constraint 13 | attr_reader :person_a, :person_b 14 | 15 | def initialize(person_a, person_b) 16 | super([person_a, person_b]) 17 | @person_a = person_a 18 | @person_b = person_b 19 | end 20 | 21 | def satisfies?(assignment = {}) 22 | return true if skip?(assignment) 23 | 24 | shirt_color_a = assignment[person_a] 25 | shirt_color_b = assignment[person_b] 26 | 27 | shirt_color_a != shirt_color_b 28 | end 29 | 30 | def skip?(assignment) 31 | !variables.all? { |variable| assignment.key?(variable) } 32 | end 33 | end 34 | 35 | class ColorConstraint < ::CSP::Constraint 36 | attr_reader :person, :color 37 | 38 | def initialize(person:, color:) 39 | super([person]) 40 | 41 | @person = person 42 | @color = color 43 | end 44 | 45 | def satisfies?(assignment = {}) 46 | return true if skip?(assignment) 47 | 48 | picked_color = assignment[person] 49 | 50 | picked_color != color 51 | end 52 | 53 | def skip?(assignment) 54 | !variables.all? { |variable| assignment.key?(variable) } 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /csp-resolver.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'lib/csp/version' 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = 'csp-resolver' 7 | spec.version = CSP::VERSION 8 | spec.license = 'MIT' 9 | spec.authors = ['André Benjamim', 'Gustavo Alberto'] 10 | spec.email = ['andre.benjamim@rebase.com.br', 'gustavo.costa@rebase.com.br'] 11 | 12 | spec.summary = 'A Ruby CSP Solver' 13 | spec.description = 'This Ruby gem solves CSPs using custom constraints' 14 | spec.homepage = 'https://github.com/Rebase-BR/csp-resolver' 15 | spec.required_ruby_version = '>= 2.5.8' 16 | 17 | spec.metadata['homepage_uri'] = spec.homepage 18 | spec.metadata['source_code_uri'] = 'https://github.com/Rebase-BR/csp-resolver' 19 | 20 | # Specify which files should be added to the gem when it is released. 21 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git. 22 | spec.files = Dir.chdir(__dir__) do 23 | `git ls-files -z`.split("\x0").reject do |f| 24 | (File.expand_path(f) == __FILE__) || 25 | f.start_with?(*%w[bin/ test/ spec/ features/ .git appveyor Gemfile]) 26 | end 27 | end 28 | spec.bindir = 'exe' 29 | spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } 30 | spec.require_paths = ['lib'] 31 | 32 | # For more information and examples about making a new gem, check out our 33 | # guide at: https://bundler.io/guides/creating_gem.html 34 | end 35 | -------------------------------------------------------------------------------- /spec/examples/sculpture/constraints/only_one_constraint_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | require_relative '../../../../examples/event_scheduling' 5 | 6 | RSpec.describe CSP::Examples::EventScheduling::OnlyOneConstraint do 7 | describe '#satisfies?' do 8 | context 'when both events are assigned different time slots' do 9 | it 'returns true' do 10 | assignment = { 11 | 0 => 0, 12 | 1 => 1 13 | } 14 | constraint = described_class.new(0, 1) 15 | expect(constraint.satisfies?(assignment)).to eq true 16 | end 17 | end 18 | 19 | context 'when both events are assigned the same time slot' do 20 | it 'returns false' do 21 | assignment = { 22 | 0 => 1, 23 | 1 => 1 24 | } 25 | constraint = described_class.new(0, 1) 26 | expect(constraint.satisfies?(assignment)).to eq false 27 | end 28 | end 29 | 30 | context 'when one of the events is not assigned a time slot' do 31 | it 'returns true' do 32 | assignment = { 33 | 0 => 1 34 | } 35 | constraint = described_class.new(0, 1) 36 | expect(constraint.satisfies?(assignment)).to eq true 37 | end 38 | end 39 | 40 | context 'when neither of the events are assigned a time slot' do 41 | it 'returns true' do 42 | assignment = {} 43 | constraint = described_class.new(0, 1) 44 | expect(constraint.satisfies?(assignment)).to eq true 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /spec/examples/sculpture/constraints/queens_constraint_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | require_relative '../../../../examples/queen' 5 | 6 | RSpec.describe CSP::Examples::Queen::QueensConstraint do 7 | describe '#satisfies?' do 8 | context 'when no two queens threaten each other' do 9 | columns = (0..8).to_a 10 | constraint = described_class.new(columns) 11 | 12 | it 'returns true' do 13 | expect(constraint.satisfies?({ 0 => 0, 1 => 4, 2 => 7, 3 => 5, 4 => 2, 5 => 6, 6 => 1, 7 => 3 })).to eq(true) 14 | end 15 | end 16 | 17 | context 'when two queens are on the same row' do 18 | columns = (0..8).to_a 19 | constraint = described_class.new(columns) 20 | 21 | it 'returns false' do 22 | expect(constraint.satisfies?({ 0 => 0, 1 => 1, 2 => 7, 3 => 5, 4 => 2, 5 => 6, 6 => 1, 7 => 3 })).to eq(false) 23 | end 24 | end 25 | 26 | context 'when two queens are on the same diagonal' do 27 | columns = (0..8).to_a 28 | constraint = described_class.new(columns) 29 | 30 | it 'returns false' do 31 | expect(constraint.satisfies?({ 0 => 0, 1 => 3, 2 => 6, 3 => 2, 4 => 5, 5 => 7, 6 => 4, 7 => 1 })).to eq(false) 32 | end 33 | end 34 | 35 | context 'with incomplete assignments' do 36 | it 'returns true' do 37 | columns = (0..8).to_a 38 | constraint = described_class.new(columns) 39 | 40 | expect(constraint.satisfies?({ 0 => 0, 1 => 3, 2 => 6 })).to eq true 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | csp-resolver (0.1.0) 5 | 6 | GEM 7 | remote: https://rubygems.org/ 8 | specs: 9 | ast (2.4.2) 10 | diff-lcs (1.5.1) 11 | docile (1.4.0) 12 | parallel (1.24.0) 13 | parser (3.3.0.5) 14 | ast (~> 2.4.1) 15 | racc 16 | racc (1.7.3) 17 | rainbow (3.1.1) 18 | rake (13.2.1) 19 | regexp_parser (2.9.0) 20 | rexml (3.2.6) 21 | rspec (3.13.0) 22 | rspec-core (~> 3.13.0) 23 | rspec-expectations (~> 3.13.0) 24 | rspec-mocks (~> 3.13.0) 25 | rspec-core (3.13.0) 26 | rspec-support (~> 3.13.0) 27 | rspec-expectations (3.13.0) 28 | diff-lcs (>= 1.2.0, < 2.0) 29 | rspec-support (~> 3.13.0) 30 | rspec-mocks (3.13.0) 31 | diff-lcs (>= 1.2.0, < 2.0) 32 | rspec-support (~> 3.13.0) 33 | rspec-support (3.13.1) 34 | rubocop (1.28.2) 35 | parallel (~> 1.10) 36 | parser (>= 3.1.0.0) 37 | rainbow (>= 2.2.2, < 4.0) 38 | regexp_parser (>= 1.8, < 3.0) 39 | rexml 40 | rubocop-ast (>= 1.17.0, < 2.0) 41 | ruby-progressbar (~> 1.7) 42 | unicode-display_width (>= 1.4.0, < 3.0) 43 | rubocop-ast (1.17.0) 44 | parser (>= 3.1.1.0) 45 | ruby-progressbar (1.13.0) 46 | simplecov (0.22.0) 47 | docile (~> 1.1) 48 | simplecov-html (~> 0.11) 49 | simplecov_json_formatter (~> 0.1) 50 | simplecov-html (0.12.3) 51 | simplecov_json_formatter (0.1.4) 52 | unicode-display_width (2.5.0) 53 | 54 | PLATFORMS 55 | x86_64-linux 56 | 57 | DEPENDENCIES 58 | csp-resolver! 59 | rake (~> 13.0) 60 | rspec 61 | rubocop (~> 1.28) 62 | simplecov 63 | 64 | BUNDLED WITH 65 | 2.3.27 66 | -------------------------------------------------------------------------------- /spec/examples/sculpture/constraints/map_coloring_constraint_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | require_relative '../../../../examples/map_coloring' 5 | 6 | RSpec.describe CSP::Examples::MapColoring::MapColoringConstraint do 7 | describe '#satisfies?' do 8 | context 'when both regions are assigned different colors' do 9 | it 'returns true' do 10 | assignment = { 11 | 'Western Australia' => 'red', 12 | 'Northern Territory' => 'blue' 13 | } 14 | constraint = described_class.new('Western Australia', 'Northern Territory') 15 | expect(constraint.satisfies?(assignment)).to eq true 16 | end 17 | end 18 | 19 | context 'when both regions are assigned the same color' do 20 | it 'returns false' do 21 | assignment = { 22 | 'Western Australia' => 'blue', 23 | 'Northern Territory' => 'blue' 24 | } 25 | constraint = described_class.new('Western Australia', 'Northern Territory') 26 | expect(constraint.satisfies?(assignment)).to eq false 27 | end 28 | end 29 | 30 | context 'when one of the regions is not assigned a color' do 31 | it 'returns true' do 32 | assignment = { 33 | 'Western Australia' => 'blue' 34 | # 'Northern Territory' is not assigned 35 | } 36 | constraint = described_class.new('Western Australia', 'Northern Territory') 37 | expect(constraint.satisfies?(assignment)).to eq true 38 | end 39 | end 40 | 41 | context 'when neither of the regions are assigned a color' do 42 | it 'returns true' do 43 | assignment = {} 44 | constraint = described_class.new('Western Australia', 'Northern Territory') 45 | expect(constraint.satisfies?(assignment)).to eq true 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Description 2 | 10 | 11 | Fixes # (issue number) 12 | 13 | ## Proposed Change 14 | 19 | 20 | ## Comments 21 | 28 | 29 | ## Type of change 30 | - [ ] Bug fix (non-breaking change which fixes an issue) 31 | - [ ] New feature (non-breaking change which adds functionality) 32 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 33 | - [ ] This change requires a documentation update 34 | 35 | 36 | # Checklist: 37 | Go over all the following points, and put an `x` in all the boxes that apply. 38 | 39 | If you're unsure about any of these, don't hesitate to ask. We're here to help! 40 | 41 | - [ ] My code follows the code style of this project 42 | - [ ] I have performed a self-review of my code 43 | - [ ] I have made corresponding changes to the documentation 44 | - [ ] I have read the **CONTRIBUTING** document 45 | - [ ] I have added tests to cover my changes 46 | - [ ] All new and existing tests passed 47 | -------------------------------------------------------------------------------- /spec/examples/sculpture/constraints/room_limit_to_one_constraint_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | require_relative '../../../../examples/sculpture' 5 | 6 | RSpec.describe CSP::Examples::Sculpture::RoomLimitToOneConstraint do 7 | describe '#satisfies?' do 8 | context 'when only one variable in the assigment has the room value' do 9 | it 'retuns true' do 10 | variable = double('Variable') 11 | variable2 = double('Variable') 12 | variable3 = double('Variable') 13 | variables = [variable, variable2, variable3] 14 | constraint = described_class.new(room: 1, variables: variables) 15 | 16 | satisfies = constraint.satisfies?({ variable => 1, variable2 => 2, variable3 => 2 }) 17 | 18 | expect(satisfies).to eq true 19 | end 20 | end 21 | 22 | context 'when none variable in the assigment have the room value' do 23 | it 'return true' do 24 | variable = double('Variable') 25 | variable2 = double('Variable') 26 | variable3 = double('Variable') 27 | variables = [variable, variable2, variable3] 28 | constraint = described_class.new(room: 1, variables: variables) 29 | 30 | satisfies = constraint.satisfies?({ variable => 3, variable2 => 2, variable3 => 2 }) 31 | 32 | expect(satisfies).to eq true 33 | end 34 | end 35 | 36 | context 'when atleast two variables in the assigment has the room value' do 37 | it 'retuns false' do 38 | variable = double('Variable') 39 | variable2 = double('Variable') 40 | variable3 = double('Variable') 41 | variables = [variable, variable2, variable] 42 | constraint = described_class.new(room: 1, variables: variables) 43 | satisfies = constraint.satisfies?({ variable => 1, variable2 => 1, variable3 => 2 }) 44 | 45 | expect(satisfies).to eq false 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /examples/sculpture.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../lib/csp-resolver' 4 | 5 | module CSP 6 | module Examples 7 | # Three sculptures (A, B, C) are to be exhibited in rooms 1,2 of an art gallery 8 | # 9 | # The exhibition must satisfy the following conditions: 10 | # 1. Sculptures A and B cannot be in the same room 11 | # 2. Sculptures B and C must be in the same room 12 | # 3. Room 2 can only hold one sculpture 13 | class Sculpture 14 | def call 15 | variables = %w[A B C] 16 | 17 | csp = CSP::Problem.new 18 | .add_variable('A', domains: [1, 2]) 19 | .add_variable('B', domains: [1, 2]) 20 | .add_variable('C', domains: [1, 2]) 21 | .unique(%w[A B]) 22 | .add_constraint(variables: %w[B C]) { |b, c| b == c } 23 | .add_constraint(RoomLimitToOneConstraint.new(room: 2, variables: variables)) 24 | solution = csp.solve 25 | 26 | solution || 'No solution found' 27 | end 28 | 29 | class CannotBeInSameRoomConstraint < ::CSP::Constraint 30 | def satisfies?(assignment) 31 | values = assignment.values_at(*variables) 32 | 33 | return true if values.any?(&:nil?) 34 | 35 | values == values.uniq 36 | end 37 | end 38 | 39 | class MustBeInSameRoomConstraint < ::CSP::Constraint 40 | def satisfies?(assignment) 41 | values = assignment.values_at(*variables) 42 | 43 | return true if values.any?(&:nil?) 44 | 45 | values.uniq.size == 1 46 | end 47 | end 48 | 49 | class RoomLimitToOneConstraint < ::CSP::Constraint 50 | attr_reader :room 51 | 52 | def initialize(room:, variables:) 53 | super(variables) 54 | @room = room 55 | end 56 | 57 | def satisfies?(assignment) 58 | values = assignment.values_at(*variables) 59 | 60 | values.count(room) <= 1 61 | end 62 | end 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /examples/map_coloring.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../lib/csp-resolver' 4 | 5 | module CSP 6 | module Examples 7 | class MapColoring 8 | def call # rubocop:disable Metrics/MethodLength 9 | variables = [ 10 | 'Western Australia', 11 | 'Northern Territory', 12 | 'South Australia', 13 | 'Queensland', 14 | 'New South Wales', 15 | 'Victoria', 16 | 'Tasmania' 17 | ] 18 | 19 | domains = %w[red blue green] 20 | 21 | csp = CSP::Problem.new 22 | .add_variables(variables, domains: domains) 23 | 24 | add_constraint(csp, 'Western Australia', 'Northern Territory') 25 | add_constraint(csp, 'Western Australia', 'South Australia') 26 | add_constraint(csp, 'South Australia', 'Northern Territory') 27 | add_constraint(csp, 'Queensland', 'Northern Territory') 28 | add_constraint(csp, 'Queensland', 'South Australia') 29 | add_constraint(csp, 'Queensland', 'New South Wales') 30 | add_constraint(csp, 'New South Wales', 'South Australia') 31 | add_constraint(csp, 'Victoria', 'South Australia') 32 | add_constraint(csp, 'Victoria', 'New South Wales') 33 | add_constraint(csp, 'Victoria', 'Tasmania') 34 | 35 | solution = csp.solve 36 | solution || 'No solution found' 37 | end 38 | 39 | def add_constraint(csp, place1, place2) 40 | csp.add_constraint(MapColoringConstraint.new(place1, place2)) 41 | end 42 | 43 | class MapColoringConstraint < ::CSP::Constraint 44 | attr_reader :place1, :place2 45 | 46 | def initialize(place1, place2) 47 | super([place1, place2]) 48 | 49 | @place1 = place1 50 | @place2 = place2 51 | end 52 | 53 | def satisfies?(assignment) 54 | # If any of them is not assigned then there's no conflict 55 | return true if variables.any? { |variable| !assignment.key?(variable) } 56 | 57 | assignment[place1] != assignment[place2] 58 | end 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /spec/examples/sculpture/constraints/cannot_be_in_same_room_constraint_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | require_relative '../../../../examples/sculpture' 5 | 6 | RSpec.describe CSP::Examples::Sculpture::CannotBeInSameRoomConstraint do 7 | describe '#satisfies?' do 8 | context 'when all assignments are different' do 9 | it 'retuns true' do 10 | variable = double('Variable') 11 | variable2 = double('Variable') 12 | variable3 = double('Variable') 13 | variables = [variable, variable2, variable3] 14 | constraint = described_class.new(variables) 15 | 16 | satisfies = constraint.satisfies?({ variable => 1, variable2 => 2, variable3 => 3 }) 17 | 18 | expect(satisfies).to eq true 19 | end 20 | end 21 | 22 | context 'when any two assignments are equal' do 23 | it 'retuns false' do 24 | variable = double('Variable') 25 | variable2 = double('Variable') 26 | variable3 = double('Variable') 27 | variables = [variable, variable2, variable3] 28 | constraint = described_class.new(variables) 29 | satisfies = constraint.satisfies?({ variable => 1, variable2 => 1, variable3 => 3 }) 30 | 31 | expect(satisfies).to eq false 32 | end 33 | end 34 | 35 | context 'when any variable is nil in the assigment' do 36 | it 'return true' do 37 | variable = double('Variable') 38 | variable2 = double('Variable') 39 | variable3 = double('Variable') 40 | variables = [variable, variable2, variable3] 41 | constraint = described_class.new(variables) 42 | 43 | satisfies = constraint.satisfies?({ variable => 1, variable2 => 1 }) 44 | 45 | expect(satisfies).to eq true 46 | end 47 | end 48 | 49 | context 'when any variable has the nil value in the assigment' do 50 | it 'return true' do 51 | variable = double('Variable') 52 | variable2 = double('Variable') 53 | variable3 = double('Variable') 54 | variables = [variable, variable2, variable3] 55 | constraint = described_class.new(variables) 56 | 57 | satisfies = constraint.satisfies?({ variable => 1, variable2 => 1, variable3 => nil }) 58 | 59 | expect(satisfies).to eq true 60 | end 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /spec/examples/sculpture/constraints/must_be_in_same_room_constraint_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | require_relative '../../../../examples/sculpture' 5 | 6 | RSpec.describe CSP::Examples::Sculpture::MustBeInSameRoomConstraint do 7 | describe '#satisfies?' do 8 | context 'when variables have equal values in the assignment' do 9 | it 'retuns true' do 10 | variable = double('Variable') 11 | variable2 = double('Variable') 12 | variable3 = double('Variable') 13 | variables = [variable, variable2] 14 | constraint = described_class.new(variables) 15 | 16 | satisfies = constraint.satisfies?({ variable => 1, variable2 => 1, variable3 => 3 }) 17 | 18 | expect(satisfies).to eq true 19 | end 20 | end 21 | 22 | context 'when variables not have equal values in the assigment' do 23 | it 'retuns false' do 24 | variable = double('Variable') 25 | variable2 = double('Variable') 26 | variable3 = double('Variable') 27 | variables = [variable, variable2] 28 | constraint = described_class.new(variables) 29 | satisfies = constraint.satisfies?({ variable => 1, variable2 => 2, variable3 => 3 }) 30 | 31 | expect(satisfies).to eq false 32 | end 33 | end 34 | 35 | context 'when any variable is nil in the assigment' do 36 | it 'return true' do 37 | variable = double('Variable') 38 | variable2 = double('Variable') 39 | variable3 = double('Variable') 40 | variables = [variable, variable2, variable3] 41 | constraint = described_class.new(variables) 42 | 43 | satisfies = constraint.satisfies?({ variable => 1, variable2 => 1 }) 44 | 45 | expect(satisfies).to eq true 46 | end 47 | end 48 | 49 | context 'when any variable has the nil value in the assigment' do 50 | it 'return true' do 51 | variable = double('Variable') 52 | variable2 = double('Variable') 53 | variable3 = double('Variable') 54 | variables = [variable, variable2, variable3] 55 | constraint = described_class.new(variables) 56 | 57 | satisfies = constraint.satisfies?({ variable => 1, variable2 => 2, variable3 => nil }) 58 | 59 | expect(satisfies).to eq true 60 | end 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thank you for your interest in contributing! We welcome contributions of all kinds, including new features, bug fixes, and documentation improvements. 4 | 5 | Please take a moment to review this document to make the contribution process easy and effective for everyone involved. 6 | 7 | If you have any questions, feel free to reach out by opening an issue or contacting the maintainers directly. 8 | 9 | ## Table of Contents 10 | 1. [How to Report an Issue](#how-to-report-an-issue) 11 | 2. [Submitting a Pull Request](#submitting-a-pull-request) 12 | 3. [Coding Guidelines](#coding-guidelines) 13 | 4. [Code of Conduct](#code-of-conduct) 14 | 15 | ## How to Report an Issue 16 | 17 | If you encounter any bugs or have feature requests, please create an issue on our GitHub repository. 18 | 19 | 1. Go to the [Issues](https://github.com/Rebase-BR/csp-resolver/issues) page. 20 | 2. Click on "New Issue". 21 | 3. Select the "Bug report" or "Feature request". 22 | 4. Fill in the details as prompted by the template. 23 | 24 | If your issue does not fit the provided templates, feel free to create an issue without following them. Provide as much detail as possible to help us understand and address your concern. 25 | 26 | ## Submitting a Pull Request 27 | To submit a pull request, please follow these steps: 28 | 29 | 1. **Fork the Repository**: 30 | - Navigate to the [repository](https://github.com/Rebase-BR/csp-resolver). 31 | - Click the "Fork" button in the top-right corner. 32 | 2. **Clone the Forked Repository**: 33 | ```sh 34 | git clone https://github.com/Rebase-BR/csp-resolver.git 35 | cd csp-resolver 36 | ``` 37 | 3. **Create a Branch**: 38 | ```sh 39 | git checkout -b 40 | ``` 41 | 4. **Make Your Changes**: 42 | - Implement your feature, bug fix, or documentation improvement. 43 | - If you're making changes to the code, write some tests. 44 | - Ensure your code follows our [Coding Guidelines](#coding-guidelines). 45 | 5. **Commit Your Changes**: 46 | ```sh 47 | git add . 48 | git commit -m "Description of your changes" 49 | ``` 50 | 6. **Push to Your Fork**: 51 | ```sh 52 | git push origin your-branch-name 53 | ``` 54 | 7. **Create a Pull Request**: 55 | - Go to your fork on GitHub. 56 | - Click on the "Compare & pull request" button. 57 | - Fill in the details of your pull request and submit. 58 | 59 | We will review your pull request and provide feedback. Please be responsive to our comments and make the necessary adjustments. 60 | 61 | ## Coding Guidelines 62 | 63 | To ensure consistency and maintainability of the codebase, please adhere to the following guidelines: 64 | 65 | - Follow the Ruby community's [style guide](https://rubystyle.guide/). 66 | - Write tests for your changes. 67 | - Ensure all tests pass before submitting a pull request. 68 | - Document public methods and classes. 69 | 70 | ## Code of Conduct 71 | 72 | By participating in this project, you agree to abide by our [Code of Conduct](./CODE_OF_CONDUCT.md). Please be respectful and considerate in all interactions. 73 | -------------------------------------------------------------------------------- /lib/csp/algorithms/lookahead/ac3.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CSP 4 | module Algorithms 5 | module Lookahead 6 | class Ac3 7 | attr_reader :problem 8 | 9 | def initialize(problem) 10 | @problem = problem 11 | end 12 | 13 | def call(variables:, assignment:, domains:) 14 | new_domains = variables.each_with_object({}) do |variable, domains_hash| 15 | variable_domains = Array(assignment[variable] || domains[variable]) 16 | 17 | domains_hash[variable] = unary_check(variable, variable_domains) 18 | end 19 | 20 | variable_arcs = arcs(variables) 21 | 22 | arc_consistency(variable_arcs, new_domains) 23 | end 24 | 25 | def arc_consistency(arcs, domains) 26 | queue = arcs.dup 27 | 28 | until queue.empty? 29 | arc, *queue = queue 30 | x, y = arc.keys.first 31 | constraint = arc.values.first 32 | 33 | next unless arc_reduce(x, y, constraint, domains) 34 | return nil if domains[x].empty? 35 | 36 | new_arcs = find_arcs(x, y, arcs) 37 | queue.push(*new_arcs) 38 | end 39 | 40 | domains 41 | end 42 | 43 | def arc_reduce(x, y, constraint, domains) # rubocop:disable Naming/MethodParameterName 44 | changed = false 45 | x_domains = domains[x] 46 | y_domains = domains[y] 47 | 48 | x_domains.each do |x_value| 49 | consistent = y_domains.any? do |y_value| 50 | sat = constraint.satisfies?({ x => x_value, y => y_value }) 51 | 52 | sat 53 | end 54 | 55 | next if consistent 56 | 57 | x_domains -= [x_value] 58 | changed = true 59 | end 60 | 61 | domains[x] = x_domains 62 | 63 | changed 64 | end 65 | 66 | # Returns all (z, x) arcs where z != y 67 | def find_arcs(x, y, arcs) # rubocop:disable Naming/MethodParameterName 68 | arcs.select do |arc| 69 | arc.any? do |(first, second), _constraint| 70 | first != y && second == x 71 | end 72 | end 73 | end 74 | 75 | # Setup arcs between variables 76 | def arcs(variables) 77 | variables.each_with_object([]) do |variable, worklist| 78 | constraints = problem.constraints[variable].select(&:binary?) 79 | 80 | constraints.each do |constraint| 81 | variables_ij = [variable] | constraint.variables # make current variable be the first 82 | 83 | worklist << { variables_ij => constraint } 84 | end 85 | end 86 | end 87 | 88 | def unary_check(variable, variable_domains) 89 | constraints = problem.constraints[variable].select(&:unary?) 90 | 91 | variable_domains.select do |domain| 92 | constraints.all? do |constraint| 93 | constraint.satisfies?({ variable => domain }) 94 | end 95 | end 96 | end 97 | end 98 | end 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /lib/csp/algorithms/backtracking.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'forwardable' 4 | 5 | module CSP 6 | module Algorithms 7 | class Backtracking 8 | extend Forwardable 9 | 10 | ORDERING_ALGORITHM = Ordering::NoOrder 11 | FILTERING_ALGORITHM = Filtering::NoFilter 12 | LOOKAHEAD_ALGORITHM = Lookahead::NoAlgorithm 13 | 14 | attr_reader :problem, :solutions, :max_solutions, 15 | :ordering_algorithm, :filtering_algorithm, :lookahead_algorithm 16 | 17 | def_delegators :problem, :variables, :constraints 18 | 19 | def initialize( 20 | problem:, 21 | ordering_algorithm: nil, 22 | filtering_algorithm: nil, 23 | lookahead_algorithm: nil, 24 | max_solutions: 1 25 | ) 26 | @problem = problem 27 | @ordering_algorithm = ordering_algorithm || ORDERING_ALGORITHM.new(problem) 28 | @filtering_algorithm = filtering_algorithm || FILTERING_ALGORITHM.new(problem) 29 | @lookahead_algorithm = lookahead_algorithm || LOOKAHEAD_ALGORITHM.new(problem) 30 | @max_solutions = max_solutions 31 | @solutions = [] 32 | end 33 | 34 | def backtracking(assignment = {}) 35 | backtracking_recursion(assignment, problem_domains) 36 | end 37 | 38 | def consistent?(variable, assignment) 39 | constraints[variable].all? do |constraint| 40 | constraint.satisfies?(assignment) 41 | end 42 | end 43 | 44 | private 45 | 46 | def problem_domains 47 | problem.domains 48 | end 49 | 50 | def backtracking_recursion(assignment, domains) 51 | return solutions if max_solutions? 52 | return add_solution(assignment) if complete?(assignment) 53 | 54 | unassigned = next_unassigned_variable(assignment) 55 | 56 | domains_for(unassigned, assignment, domains).each do |value| 57 | local_assignment = assignment.clone 58 | local_assignment[unassigned] = value 59 | 60 | next unless consistent?(unassigned, local_assignment) 61 | 62 | new_domains = lookahead(local_assignment, domains) 63 | 64 | next unless new_domains 65 | 66 | backtracking_recursion(local_assignment, new_domains) 67 | 68 | return solutions if max_solutions? 69 | end 70 | 71 | [] 72 | end 73 | 74 | def add_solution(assignment) 75 | solutions << assignment 76 | end 77 | 78 | def max_solutions? 79 | solutions.size >= max_solutions 80 | end 81 | 82 | def complete?(assignment) 83 | assignment.size == variables.size 84 | end 85 | 86 | def next_unassigned_variable(assignment) 87 | unassigned_variables(assignment).first 88 | end 89 | 90 | def unassigned_variables(assignment) 91 | variables 92 | .reject { |variable| assignment.key?(variable) } 93 | .yield_self { |v| ordering_algorithm.call(v) } 94 | end 95 | 96 | def domains_for(unassigned, assignment, domains) 97 | filtering_algorithm.call( 98 | values: domains[unassigned], 99 | assignment_values: assignment.values.flatten 100 | ) 101 | end 102 | 103 | def lookahead(assignment, domains) 104 | lookahead_algorithm.call( 105 | variables: variables, 106 | assignment: assignment, 107 | domains: domains 108 | ) 109 | end 110 | end 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /lib/csp/problem.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CSP 4 | # TODO: implement dependent factor with weight 5 | # TODO: implement lookahead, arc-consistency, ac3 6 | class Problem 7 | include CSP::Constraints 8 | 9 | attr_reader :variables, :domains, :constraints, :max_solutions, 10 | :ordering_algorithm, :filtering_algorithm, :lookahead_algorithm 11 | 12 | InvalidConstraintVariable = Class.new(StandardError) 13 | VariableShouldNotBeEmpty = Class.new(StandardError) 14 | DomainsShouldNotBeEmpty = Class.new(StandardError) 15 | VariableAlreadySeted = Class.new(StandardError) 16 | 17 | def initialize(max_solutions: 1) 18 | @variables = [] 19 | @domains = {} 20 | @constraints = {} 21 | @max_solutions = max_solutions 22 | end 23 | 24 | def solve(assignment = {}) 25 | Utils::Array.wrap(search_solution(assignment)) 26 | end 27 | 28 | def add_variable(variable, domains:) 29 | if (variable.respond_to?(:empty?) && variable.empty?) || variable.nil? 30 | raise VariableShouldNotBeEmpty, 'Variable was empty in the function parameter' 31 | end 32 | raise DomainsShouldNotBeEmpty, 'Domains was empty in the function parameter' if domains.empty? 33 | raise VariableAlreadySeted, "Variable #{variable} has already been seted" if variables.include?(variable) 34 | 35 | variables << variable 36 | @domains[variable] = domains 37 | constraints[variable] = [] 38 | 39 | self 40 | end 41 | 42 | def add_variables(variables, domains:) 43 | variables.each do |variable| 44 | add_variable(variable, domains: domains) 45 | end 46 | 47 | self 48 | end 49 | 50 | def add_constraint(constraint = nil, variables: nil, &block) 51 | validate_parameters(constraint, variables, block) 52 | 53 | constraint = CustomConstraint.new(variables, block) if block 54 | 55 | constraint.variables.each do |variable| 56 | next constraints[variable] << constraint if constraints.include?(variable) 57 | 58 | raise InvalidConstraintVariable, 59 | "Constraint's variable doesn't exists in CSP" 60 | end 61 | 62 | self 63 | end 64 | 65 | def add_ordering(ordering_algorithm) 66 | @ordering_algorithm = ordering_algorithm 67 | self 68 | end 69 | 70 | def add_filtering(filtering_algorithm) 71 | @filtering_algorithm = filtering_algorithm 72 | self 73 | end 74 | 75 | def add_lookahead(lookahead_algorithm) 76 | @lookahead_algorithm = lookahead_algorithm 77 | self 78 | end 79 | 80 | private 81 | 82 | def validate_parameters(constraint, variables, block) 83 | if missing_both_constraint_and_block?(constraint, block) 84 | raise ArgumentError, 'Either constraint or block must be provided' 85 | end 86 | if provided_both_constraint_and_block?(constraint, block) 87 | raise ArgumentError, 'Both constraint and block cannot be provided at the same time' 88 | end 89 | if missing_variables_for_block?(block, variables) 90 | raise ArgumentError, 'Variables must be provided when using a block' 91 | end 92 | return unless block_arity_exceeds_variables?(block, variables) 93 | 94 | raise ArgumentError, 'Block should not have more arity than the quantity of variables' 95 | end 96 | 97 | def missing_both_constraint_and_block?(constraint, block) 98 | constraint.nil? && block.nil? 99 | end 100 | 101 | def provided_both_constraint_and_block?(constraint, block) 102 | constraint && block 103 | end 104 | 105 | def missing_variables_for_block?(block, variables) 106 | block && variables.nil? 107 | end 108 | 109 | def block_arity_exceeds_variables?(block, variables) 110 | !variables.nil? && block.arity > variables.length 111 | end 112 | 113 | def search_solution(assignment = {}) 114 | algorithm.backtracking(assignment) 115 | end 116 | 117 | def algorithm 118 | Algorithms::Backtracking.new( 119 | problem: self, 120 | ordering_algorithm: ordering_algorithm, 121 | filtering_algorithm: filtering_algorithm, 122 | lookahead_algorithm: lookahead_algorithm, 123 | max_solutions: max_solutions 124 | ) 125 | end 126 | end 127 | end 128 | -------------------------------------------------------------------------------- /spec/csp/constraints_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe CSP::Constraints::AllDifferentConstraint do 6 | describe '#satisfies?' do 7 | context 'when all assignments are different' do 8 | it 'retuns true' do 9 | variable = double('Variable') 10 | variable2 = double('Variable') 11 | variable3 = double('Variable') 12 | constraint = described_class.new 13 | 14 | satisfies = constraint.satisfies?({ variable => 1, variable2 => 2, variable3 => 3 }) 15 | 16 | expect(satisfies).to eq true 17 | end 18 | end 19 | context 'when one assigment is equal to other one' do 20 | it 'return false' do 21 | variable = double('Variable') 22 | variable2 = double('Variable') 23 | variable3 = double('Variable') 24 | constraint = described_class.new 25 | 26 | satisfies = constraint.satisfies?({ variable => 1, variable2 => 1, variable3 => 3 }) 27 | 28 | expect(satisfies).to eq false 29 | end 30 | end 31 | context 'when more than one assigment is equal to other one' do 32 | it 'return false' do 33 | variable = double('Variable') 34 | variable2 = double('Variable') 35 | variable3 = double('Variable') 36 | constraint = described_class.new 37 | 38 | satisfies = constraint.satisfies?({ variable => 1, variable2 => 1, variable3 => 1 }) 39 | 40 | expect(satisfies).to eq false 41 | end 42 | end 43 | end 44 | end 45 | 46 | RSpec.describe CSP::Constraints::UniqueConstraint do 47 | describe '#satisfies?' do 48 | context 'when the assigments are unique given certain variables' do 49 | it 'retuns true' do 50 | variable = double('Variable') 51 | variable2 = double('Variable') 52 | variable3 = double('Variable') 53 | variables = [variable, variable2] 54 | constraint = described_class.new(variables) 55 | 56 | satisfies = constraint.satisfies?({ variable => 1, variable2 => 2, variable3 => 2 }) 57 | 58 | expect(satisfies).to eq true 59 | end 60 | end 61 | context 'when the assigments are not unique given certain variables' do 62 | it 'return false' do 63 | variable = double('Variable') 64 | variable2 = double('Variable') 65 | variable3 = double('Variable') 66 | variables = [variable, variable2] 67 | constraint = described_class.new(variables) 68 | 69 | satisfies = constraint.satisfies?({ variable => 1, variable2 => 1, variable3 => 3 }) 70 | 71 | expect(satisfies).to eq false 72 | end 73 | end 74 | 75 | context 'when the assigments are not unique given certain variables' do 76 | it 'return false' do 77 | variable = double('Variable') 78 | variable2 = double('Variable') 79 | variable3 = double('Variable') 80 | variables = [variable, variable2] 81 | constraint = described_class.new(variables) 82 | 83 | satisfies = constraint.satisfies?({ variable => 1, variable2 => 1, variable3 => 3 }) 84 | 85 | expect(satisfies).to eq false 86 | end 87 | end 88 | end 89 | end 90 | 91 | RSpec.describe CSP::Constraints::CustomConstraint do 92 | describe '#satisfies?' do 93 | context 'when given a block and an assigment that satisfies the custom constraint' do 94 | it 'retuns true' do 95 | variable = double('Variable') 96 | variable2 = double('Variable') 97 | variable3 = double('Variable') 98 | variables = [variable, variable3] 99 | 100 | block = proc { |var, var3| var == var3 } 101 | constraint = described_class.new(variables, block) 102 | 103 | satisfies = constraint.satisfies?({ variable => 1, variable2 => 2, variable3 => 1 }) 104 | 105 | expect(satisfies).to eq true 106 | end 107 | end 108 | context 'when given a block and an assigment that not satisfies the custom constraint' do 109 | it 'return false' do 110 | variable = double('Variable') 111 | variable2 = double('Variable') 112 | variable3 = double('Variable') 113 | variables = [variable, variable3] 114 | 115 | block = proc { |var, var3| var == var3 } 116 | constraint = described_class.new(variables, block) 117 | 118 | satisfies = constraint.satisfies?({ variable => 1, variable2 => 1, variable3 => 3 }) 119 | 120 | expect(satisfies).to eq false 121 | end 122 | end 123 | end 124 | end 125 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | require 'simplecov' 5 | require_relative '../lib/csp-resolver' 6 | 7 | if ENV['COVERAGE'] 8 | SimpleCov.start do 9 | add_filter '/spec' 10 | 11 | add_group 'CSP', '/lib/csp' 12 | add_group 'CSP Algorithms', '/lib/csp/algorithms' 13 | add_group 'CSP Examples', '/examples' 14 | end 15 | end 16 | 17 | Dir['./spec/shared/**/*.rb'].sort.each { |file| require file } 18 | 19 | # This file was generated by the `rspec --init` command. Conventionally, all 20 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. 21 | # The generated `.rspec` file contains `--require spec_helper` which will cause 22 | # this file to always be loaded, without a need to explicitly require it in any 23 | # files. 24 | # 25 | # Given that it is always loaded, you are encouraged to keep this file as 26 | # light-weight as possible. Requiring heavyweight dependencies from this file 27 | # will add to the boot time of your test suite on EVERY test run, even for an 28 | # individual file that may not need all of that loaded. Instead, consider making 29 | # a separate helper file that requires the additional dependencies and performs 30 | # the additional setup, and require it from the spec files that actually need 31 | # it. 32 | # 33 | # See https://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 34 | 35 | RSpec.configure do |config| 36 | # rspec-expectations config goes here. You can use an alternate 37 | # assertion/expectation library such as wrong or the stdlib/minitest 38 | # assertions if you prefer. 39 | config.expect_with :rspec do |expectations| 40 | # This option will default to `true` in RSpec 4. It makes the `description` 41 | # and `failure_message` of custom matchers include text for helper methods 42 | # defined using `chain`, e.g.: 43 | # be_bigger_than(2).and_smaller_than(4).description 44 | # # => "be bigger than 2 and smaller than 4" 45 | # ...rather than: 46 | # # => "be bigger than 2" 47 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 48 | end 49 | 50 | # rspec-mocks config goes here. You can use an alternate test double 51 | # library (such as bogus or mocha) by changing the `mock_with` option here. 52 | config.mock_with :rspec do |mocks| 53 | # Prevents you from mocking or stubbing a method that does not exist on 54 | # a real object. This is generally recommended, and will default to 55 | # `true` in RSpec 4. 56 | mocks.verify_partial_doubles = true 57 | end 58 | 59 | # This option will default to `:apply_to_host_groups` in RSpec 4 (and will 60 | # have no way to turn it off -- the option exists only for backwards 61 | # compatibility in RSpec 3). It causes shared context metadata to be 62 | # inherited by the metadata hash of host groups and examples, rather than 63 | # triggering implicit auto-inclusion in groups with matching metadata. 64 | config.shared_context_metadata_behavior = :apply_to_host_groups 65 | 66 | # The settings below are suggested to provide a good initial experience 67 | # with RSpec, but feel free to customize to your heart's content. 68 | # # This allows you to limit a spec run to individual examples or groups 69 | # # you care about by tagging them with `:focus` metadata. When nothing 70 | # # is tagged with `:focus`, all examples get run. RSpec also provides 71 | # # aliases for `it`, `describe`, and `context` that include `:focus` 72 | # # metadata: `fit`, `fdescribe` and `fcontext`, respectively. 73 | # config.filter_run_when_matching :focus 74 | # 75 | # # Allows RSpec to persist some state between runs in order to support 76 | # # the `--only-failures` and `--next-failure` CLI options. We recommend 77 | # # you configure your source control system to ignore this file. 78 | # config.example_status_persistence_file_path = "spec/examples.txt" 79 | # 80 | # # Limits the available syntax to the non-monkey patched syntax that is 81 | # # recommended. For more details, see: 82 | # # https://rspec.info/features/3-12/rspec-core/configuration/zero-monkey-patching-mode/ 83 | # config.disable_monkey_patching! 84 | # 85 | # # This setting enables warnings. It's recommended, but in some cases may 86 | # # be too noisy due to issues in dependencies. 87 | # config.warnings = true 88 | # 89 | # # Many RSpec users commonly either run the entire suite or an individual 90 | # # file, and it's useful to allow more verbose output when running an 91 | # # individual spec file. 92 | # if config.files_to_run.one? 93 | # # Use the documentation formatter for detailed output, 94 | # # unless a formatter has already been configured 95 | # # (e.g. via a command-line flag). 96 | # config.default_formatter = "doc" 97 | # end 98 | # 99 | # # Print the 10 slowest examples and example groups at the 100 | # # end of the spec run, to help surface which specs are running 101 | # # particularly slow. 102 | # config.profile_examples = 10 103 | # 104 | # # Run specs in random order to surface order dependencies. If you find an 105 | # # order dependency and want to debug it, you can fix the order by providing 106 | # # the seed, which is printed after each run. 107 | # # --seed 1234 108 | # config.order = :random 109 | # 110 | # # Seed global randomization in this process using the `--seed` CLI option. 111 | # # Setting this allows you to use `--seed` to deterministically reproduce 112 | # # test failures related to randomization by passing the same `--seed` value 113 | # # as the one that triggered the failure. 114 | # Kernel.srand config.seed 115 | end 116 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, caste, color, religion, or sexual 10 | identity and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the overall 26 | community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or advances of 31 | any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email address, 35 | without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official email address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | devs@rebase.com.br. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series of 86 | actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or permanent 93 | ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within the 113 | community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.1, available at 119 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 120 | 121 | Community Impact Guidelines were inspired by 122 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 123 | 124 | For answers to common questions about this code of conduct, see the FAQ at 125 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at 126 | [https://www.contributor-covenant.org/translations][translations]. 127 | 128 | [homepage]: https://www.contributor-covenant.org 129 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 130 | [Mozilla CoC]: https://github.com/mozilla/diversity 131 | [FAQ]: https://www.contributor-covenant.org/faq 132 | [translations]: https://www.contributor-covenant.org/translations 133 | 134 | -------------------------------------------------------------------------------- /spec/csp/algorithms/lookahead/ac3_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../../../support/tshirt' 4 | 5 | RSpec.describe CSP::Algorithms::Lookahead::Ac3 do 6 | describe '#call' do 7 | it 'returns new domains that satisfies the unary constraints' do 8 | color_constraint = Tshirt::ColorConstraint 9 | 10 | problem = Tshirt.new 11 | variables = problem.people # A B C 12 | domain_values = problem.colors # red green blue 13 | domains = variables.product([domain_values]).to_h # { A => [red, green, blue] } 14 | 15 | constraint_a_not_blue = color_constraint.new(person: 'A', color: 'blue') 16 | constraint_b_not_blue = color_constraint.new(person: 'B', color: 'blue') 17 | constraint_b_not_red = color_constraint.new(person: 'B', color: 'red') 18 | constraint_c_not_red = color_constraint.new(person: 'C', color: 'red') 19 | 20 | problem = ::CSP::Problem.new 21 | .add_variables(variables, domains: domains) 22 | .add_constraint(constraint_a_not_blue) 23 | .add_constraint(constraint_b_not_blue) 24 | .add_constraint(constraint_b_not_red) 25 | .add_constraint(constraint_c_not_red) 26 | 27 | algorithm = described_class.new(problem) 28 | 29 | new_domains = algorithm.call( 30 | variables: variables, 31 | assignment: {}, 32 | domains: domains 33 | ) 34 | 35 | expect(new_domains).to eq( 36 | { 37 | 'A' => %w[red green], 38 | 'B' => %w[green], 39 | 'C' => %w[green blue] 40 | } 41 | ) 42 | end 43 | 44 | context 'when receives an assigment' do 45 | it 'returns new domains that satisfies the unary constraints' do 46 | color_constraint = Tshirt::ColorConstraint 47 | 48 | problem = Tshirt.new 49 | variables = problem.people # A B C 50 | domain_values = problem.colors # red green blue 51 | domains = variables.product([domain_values]).to_h # { A => [red, green, blue] } 52 | 53 | constraint_a_not_blue = color_constraint.new(person: 'A', color: 'blue') 54 | constraint_b_not_blue = color_constraint.new(person: 'B', color: 'blue') 55 | constraint_b_not_red = color_constraint.new(person: 'B', color: 'red') 56 | constraint_c_not_red = color_constraint.new(person: 'C', color: 'red') 57 | 58 | problem = ::CSP::Problem.new 59 | .add_variables(variables, domains: domains) 60 | .add_constraint(constraint_a_not_blue) 61 | .add_constraint(constraint_b_not_blue) 62 | .add_constraint(constraint_b_not_red) 63 | .add_constraint(constraint_c_not_red) 64 | 65 | algorithm = described_class.new(problem) 66 | 67 | new_domains = algorithm.call( 68 | variables: variables, 69 | assignment: { 'A' => 'red' }, 70 | domains: domains 71 | ) 72 | 73 | expect(new_domains).to eq( 74 | { 75 | 'A' => %w[red], 76 | 'B' => %w[green], 77 | 'C' => %w[green blue] 78 | } 79 | ) 80 | end 81 | end 82 | 83 | context 'when it has binary constraints' do 84 | it 'returns new domains that satisfies the binary constraints' do 85 | color_constraint = Tshirt::ColorConstraint 86 | unique_constraint = Tshirt::UniqueConstraint 87 | 88 | problem = Tshirt.new 89 | variables = problem.people # A B C 90 | domain_values = problem.colors # red green blue 91 | domains = variables.product([domain_values]).to_h # { A => [red, green, blue] } 92 | 93 | constraint_a_not_blue = color_constraint.new(person: 'A', color: 'blue') 94 | constraint_b_not_blue = color_constraint.new(person: 'B', color: 'blue') 95 | constraint_b_not_red = color_constraint.new(person: 'B', color: 'red') 96 | constraint_c_not_red = color_constraint.new(person: 'C', color: 'red') 97 | constraint_b_diff_a = unique_constraint.new('B', 'A') 98 | constraint_b_diff_c = unique_constraint.new('B', 'C') 99 | constraint_a_diff_c = unique_constraint.new('A', 'C') 100 | 101 | problem = ::CSP::Problem.new 102 | .add_variables(variables, domains: domains) 103 | .add_constraint(constraint_a_not_blue) 104 | .add_constraint(constraint_b_not_blue) 105 | .add_constraint(constraint_b_not_red) 106 | .add_constraint(constraint_c_not_red) 107 | .add_constraint(constraint_b_diff_a) 108 | .add_constraint(constraint_b_diff_c) 109 | .add_constraint(constraint_a_diff_c) 110 | 111 | algorithm = described_class.new(problem) 112 | 113 | new_domains = algorithm.call( 114 | variables: variables, 115 | assignment: { 'A' => 'red' }, 116 | domains: domains 117 | ) 118 | 119 | expect(new_domains).to eq( 120 | { 121 | 'A' => %w[red], 122 | 'B' => %w[green], 123 | 'C' => %w[blue] 124 | } 125 | ) 126 | end 127 | end 128 | 129 | context 'when it is inconsistent' do 130 | it 'returns nil' do 131 | color_constraint = Tshirt::ColorConstraint 132 | unique_constraint = Tshirt::UniqueConstraint 133 | 134 | problem = Tshirt.new 135 | variables = problem.people # A B C 136 | domain_values = problem.colors # red green blue 137 | domains = variables.product([domain_values]).to_h # { A => [red, green, blue] } 138 | 139 | constraint_a_not_blue = color_constraint.new(person: 'A', color: 'blue') 140 | constraint_b_not_blue = color_constraint.new(person: 'B', color: 'blue') 141 | constraint_b_not_red = color_constraint.new(person: 'B', color: 'red') 142 | constraint_c_not_red = color_constraint.new(person: 'C', color: 'red') 143 | constraint_b_diff_a = unique_constraint.new('B', 'A') 144 | constraint_b_diff_c = unique_constraint.new('B', 'C') 145 | constraint_a_diff_c = unique_constraint.new('A', 'C') 146 | 147 | problem = ::CSP::Problem.new 148 | .add_variables(variables, domains: domains) 149 | .add_constraint(constraint_a_not_blue) 150 | .add_constraint(constraint_b_not_blue) 151 | .add_constraint(constraint_b_not_red) 152 | .add_constraint(constraint_c_not_red) 153 | .add_constraint(constraint_b_diff_a) 154 | .add_constraint(constraint_b_diff_c) 155 | .add_constraint(constraint_a_diff_c) 156 | 157 | algorithm = described_class.new(problem) 158 | 159 | new_domains = algorithm.call( 160 | variables: variables, 161 | assignment: { 'A' => 'green' }, 162 | domains: domains 163 | ) 164 | 165 | expect(new_domains).to be_nil 166 | end 167 | end 168 | end 169 | end 170 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CSP Resolver 2 | The `csp-resolver` gem is a powerful tool designed to solve [Constraint Satisfaction Problems](https://en.wikipedia.org/wiki/Constraint_satisfaction_problem) (CSPs), which are mathematical questions defined by strict constraints that must be met. This tool is suitable for a wide range of applications, from scheduling and planning to configuring complex systems. 3 | 4 | ## Getting Started 5 | ### Requirements 6 | **Ruby** >= 2.5.8 7 | 8 | ### Installing 9 | You can install using the following command: 10 | ```bash 11 | gem install "csp-resolver" 12 | ``` 13 | 14 | If you prefer using Bundler, add the following line to your Gemfile: 15 | ```bash 16 | gem "csp-resolver" 17 | ``` 18 | 19 | Then install it: 20 | ```bash 21 | $ bundle install 22 | ``` 23 | 24 | ## Usage 25 | 26 | ### **Setup a problem to be solved** 27 | To setup a problem we need to require the gem and initialize the CSP: 28 | 29 | ```ruby 30 | require 'csp-resolver' 31 | 32 | problem = CSP::Problem.new 33 | ``` 34 | 35 | ### **Adding variables and domains** 36 | 37 | To add variables and domains you can use the **`add_variable`** method: 38 | 39 | ```ruby 40 | variable = 'A' 41 | domains = %w[red green blue] 42 | 43 | problem.add_variable(variable, domains: domains) 44 | ``` 45 | 46 | If some variables share the same domains, you can use **`add_variables`** (plural) for easier setup: 47 | ```ruby 48 | variables = %w[B C] 49 | domains = %w[red green blue] 50 | 51 | problem.add_variables(variables, domains: domains) 52 | 53 | # is the same as 54 | problem.add_variable('B', domains: domains) 55 | problem.add_variable('C', domains: domains) 56 | ``` 57 | 58 | ### **Adding constraints** 59 | There are three ways of adding a constraint: **built-in methods**, **custom block**, or **custom constraint class**. 60 | 61 | #### **Using built-in methods** 62 | Setting a list of variables to be unique between them: 63 | ```ruby 64 | # A != B != C 65 | problem.unique(%w[A B]) 66 | problem.unique(%w[A C]) 67 | problem.unique(%w[C B]) 68 | 69 | # same as 70 | problem.unique(%w[A B C]) 71 | ``` 72 | 73 | Setting all variable assignments to be different: 74 | ```ruby 75 | # A != B != C 76 | # It will consider all variables of CSP automatically 77 | problem.all_different 78 | ``` 79 | 80 | #### **Using a custom block** 81 | You can use the **`add_constraint`** method passing `variables` and a block to create custom validations: 82 | 83 | ```ruby 84 | # Set B != C and B != A 85 | problem.add_constraint(variables: %w[B C A]) { |b, c, a| b != c && b != a } 86 | ``` 87 | 88 | The block parameters should correspond to the order of the variables provided. 89 | 90 | #### **Using a custom constraint class** 91 | To create a custom constraint class it'll need to answer if an assignment satisfies a condition for a group of variables. 92 | 93 | The easiest way to do this is inheriting from **`CSP::Constraint`**: 94 | 95 | ```ruby 96 | class MyCustomConstraint < CSP::Constraint 97 | end 98 | ``` 99 | 100 | Now the **`CustomConstraint`** can receive a list of variables which we will use to check if their assigned values conform to the constraint's rule. 101 | 102 | ```ruby 103 | variables = %w[A B C] 104 | 105 | constraint = MyCustomConstraint.new(variables) 106 | 107 | # It can answer the arity for constraint 108 | constraint.unary? # => false 109 | constraint.binary? # => false 110 | constraint.arity # => 3 111 | ``` 112 | 113 | ##### **Implementing the constraint rule** 114 | To determinate if the solution satisfies or not a constraint we need to implement the **`satisfies?`** method. This method receives a hash containing the current variables assignments. 115 | 116 | ```ruby 117 | # Variables can't have the color purple 118 | 119 | class MyCustomConstraint < CSP::Constraint 120 | def satisfies?(assignment = {}) 121 | # While not all variables for this constraint are assigned, 122 | # consider that it doesn't violates the constraint. 123 | return true if variables.all? { |variable| assignment[variable] } 124 | 125 | variables.all? { |variable| assignment[variable] != 'purple' } 126 | end 127 | end 128 | ``` 129 | 130 | ##### **Adding the constraint to CSP** 131 | To add the constraint we must instantiate it and pass the object to **`add_constraint`**: 132 | ```ruby 133 | problem = CSP::Problem.new 134 | problem.add_variables(%w[A B C], domains: %w[purple red green blue]) 135 | 136 | # B can't have the color purple 137 | constraint = MyCustomConstraint.new(%w[B]) 138 | 139 | # Add the B != purple constraint 140 | problem.add_constraint(constraint) 141 | ``` 142 | 143 | ##### **The constructor** 144 | 145 | The default constructor expects to receive an array of variables to apply the constraint. 146 | 147 | ```ruby 148 | class CSP::Constraint 149 | def initialize(variables) 150 | @variables = variables 151 | end 152 | end 153 | ``` 154 | 155 | But if you need to add other properties besides the variables, you can override the constructor: 156 | 157 | ```ruby 158 | # Instead of only purple, now we can choose which color to exclude. 159 | class MyCustomConstraint < CSP::Constraint 160 | def initialize(letters:, color:) 161 | # set letters as the variables 162 | super(letters) 163 | 164 | @letters = letters 165 | @color = color 166 | end 167 | 168 | def satisfies?(assignment = {}) 169 | # since letters is the same as variables, we can usem them interchangeably here. 170 | return true if @letters.all? { |letter| assignment[letter].present? } 171 | 172 | # we compare with the color set 173 | @letters.all? { |letter| assignment[letter] != @color } 174 | end 175 | end 176 | ``` 177 | 178 | And now we can use as we see fit: 179 | ```ruby 180 | problem = CSP::Problem.new 181 | problem.add_variables(%w[A B C], domains: %w[purple red green blue]) 182 | 183 | a_cant_be_green = MyCustomConstraint.new(letters: %w[A], color: 'green') 184 | b_cant_be_purple = MyCustomConstraint.new(letters: %w[B], color: 'purple') 185 | c_cant_be_blue = MyCustomConstraint.new(letters: %w[C], color: 'blue') 186 | 187 | problem.add_constraint(a_cant_be_green) 188 | problem.add_constraint(b_cant_be_purple) 189 | problem.add_constraint(c_cant_be_blue) 190 | ``` 191 | 192 | ##### **TL;DR** 193 | * Inherit from `CSP::Constraint` 194 | * Implement a `satisfies?(assignment = {})` that returns a boolean 195 | * Override the initializer if needed, but pass to `super` the constraint's variables 196 | 197 | ### Solving the problem 198 | After setting the problem we can search for the solution by calling `solve`: 199 | 200 | ```ruby 201 | problem.solve 202 | # => { 'A' => 'green', 'B' => 'red', 'C' => 'purple' } 203 | ``` 204 | 205 | ### Full Example: 206 | 207 | ```ruby 208 | # Given the letters A-C, pick a color between red, blue, green, and purple for them. 209 | # Consider the following rules: 210 | # * Each letters has a unique color 211 | # * A can't have the color purple 212 | # * A and C can't have the color red 213 | # * B can't have the color purple nor green 214 | 215 | # Create a constraint class 216 | class MyCustomConstraint < CSP::Constraint 217 | def initialize(letters:, color:) 218 | super(letters) 219 | @letters = letters 220 | @color = color 221 | end 222 | 223 | def satisfies?(assignment = {}) 224 | @letters.all? { |letter| assignment[letter] != @color } 225 | end 226 | end 227 | 228 | # Initialize the problem 229 | problem = CSP::Problem.new 230 | 231 | # Define the letters as variables and colors as domains 232 | variables = %w[A B C] 233 | domains = %w[purple red green blue] 234 | 235 | # Create constraints using the custom class 236 | a_cant_be_purple = MyCustomConstraint.new(letters: %w[A], color: 'purple') 237 | a_and_c_cant_be_red = MyCustomConstraint.new(letters: %w[A C], color: 'red') 238 | 239 | # Add variables and domains 240 | problem.add_variables(variables, domains: domains) 241 | 242 | # Set the unique color constraint 243 | problem.all_different 244 | 245 | # set A != purple constraint 246 | problem.add_constraint(a_cant_be_purple) 247 | 248 | # set A != purple && C != purple constraint 249 | problem.add_constraint(a_and_c_cant_be_red) 250 | 251 | # set B != purple && B != green constraint 252 | problem.add_constraint(variables: %w[B]) { |b| b != 'purple' && b != 'green' } 253 | 254 | # find the solution 255 | problem.solve 256 | # => { 'A' => 'green', 'B' => 'red', 'C' => 'purple' } 257 | ``` 258 | 259 | ## Contributing 260 | See our [CONTRIBUTING](./CONTRIBUTING.md) guidelines. 261 | 262 | ## Code of Conduct 263 | We expect that everyone participating in any way with this project follows our [Code of Conduct](./CODE_OF_CONDUCT.md). 264 | 265 | ## License 266 | This project is licensed under the [MIT License](MIT-LICENSE). 267 | -------------------------------------------------------------------------------- /spec/csp/algorithms/backtracking_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe CSP::Algorithms::Backtracking do 6 | describe '#backtracking' do 7 | context 'when domain assignment is consistent' do 8 | it 'pass the new assignment with domain to backtracking' do 9 | variable = double('Variable') 10 | variable2 = double('Variable') 11 | variables = [variable, variable2] 12 | domains = { variable => [1], variable2 => [10] } 13 | 14 | constraint = double('constraint', satisfies?: true) 15 | constraint2 = double('constraint', satisfies?: false) 16 | constraints = { variable => [constraint], variable2 => [constraint2] } 17 | 18 | problem = double('Problem', variables: variables, domains: domains, constraints: constraints) 19 | 20 | algorithm = described_class.new(problem: problem) 21 | 22 | allow(algorithm).to receive(:backtracking_recursion).and_call_original 23 | 24 | expect(algorithm.backtracking).to be_empty 25 | expect(algorithm).to have_received(:backtracking_recursion).with({}, domains) 26 | expect(algorithm).to have_received(:backtracking_recursion).with({ variable => 1 }, domains) 27 | end 28 | 29 | context 'when backtracking returns a solution' do 30 | it 'returns the solution to the call stack' do 31 | variable = double('Variable') 32 | variable2 = double('Variable') 33 | variables = [variable, variable2] 34 | 35 | domains = { variable => [1], variable2 => [10] } 36 | 37 | constraint = double('constraint', satisfies?: true) 38 | constraint2 = double('constraint', satisfies?: true) 39 | constraints = { variable => [constraint], variable2 => [constraint2] } 40 | 41 | problem = double('Problem', variables: variables, domains: domains, constraints: constraints) 42 | 43 | algorithm = described_class.new(problem: problem) 44 | 45 | allow(algorithm).to receive(:backtracking_recursion).and_call_original 46 | 47 | expect(algorithm.backtracking).to eq [variable => 1, variable2 => 10] 48 | expect(algorithm).to have_received(:backtracking_recursion).with({}, domains) 49 | expect(algorithm).to have_received(:backtracking_recursion).with({ variable => 1 }, domains) 50 | expect(algorithm).to have_received(:backtracking_recursion).with( 51 | { 52 | variable => 1, 53 | variable2 => 10 54 | }, domains 55 | ) 56 | end 57 | end 58 | 59 | context 'when backtracking returns nil' do 60 | context 'and variable has more domains to verify' do 61 | it 'attemps the next value' do 62 | variable = double('Variable') 63 | variable2 = double('Variable') 64 | variables = [variable, variable2] 65 | 66 | domains = { variable => [1, 2], variable2 => [10] } 67 | 68 | constraint = double('constraint', satisfies?: true) 69 | constraint2 = double('constraint') 70 | constraints = { variable => [constraint], variable2 => [constraint2] } 71 | 72 | problem = double('Problem', variables: variables, domains: domains, constraints: constraints) 73 | 74 | algorithm = described_class.new(problem: problem) 75 | 76 | allow(constraint2).to receive(:satisfies?).and_return(false, true) 77 | allow(algorithm).to receive(:backtracking_recursion).and_call_original 78 | 79 | expect(algorithm.backtracking).to eq [variable => 2, variable2 => 10] 80 | expect(algorithm).to have_received(:backtracking_recursion).with({}, domains) 81 | expect(algorithm).to have_received(:backtracking_recursion).with({ variable => 1 }, domains) 82 | expect(algorithm).to have_received(:backtracking_recursion).with({ variable => 2 }, domains) 83 | expect(algorithm).to have_received(:backtracking_recursion).with({ 84 | variable => 2, 85 | variable2 => 10 86 | }, domains) 87 | end 88 | end 89 | 90 | context 'and variable has no more domains to verify' do 91 | it 'returns nil' do 92 | variable = double('Variable') 93 | variable2 = double('Variable') 94 | variables = [variable, variable2] 95 | 96 | domains = { variable => [1, 2], variable2 => [10] } 97 | 98 | constraint = double('constraint', satisfies?: true) 99 | constraint2 = double('constraint', satisfies?: false) 100 | constraints = { variable => [constraint], variable2 => [constraint2] } 101 | 102 | problem = double('Problem', variables: variables, domains: domains, constraints: constraints) 103 | 104 | algorithm = described_class.new(problem: problem) 105 | 106 | allow(algorithm).to receive(:backtracking_recursion).and_call_original 107 | 108 | expect(algorithm.backtracking).to be_empty 109 | expect(algorithm).to have_received(:backtracking_recursion).with({}, domains) 110 | expect(algorithm).to have_received(:backtracking_recursion).with({ variable => 1 }, domains) 111 | end 112 | end 113 | end 114 | end 115 | 116 | context 'when no domain assignment is consistent' do 117 | it 'returns nil' do 118 | variable = double('Variable') 119 | variables = [variable] 120 | 121 | domains = { variable => [1, 2, 3, 4] } 122 | 123 | constraint = double('constraint', satisfies?: false) 124 | constraints = { variable => [constraint] } 125 | 126 | problem = double('Problem', variables: variables, domains: domains, constraints: constraints) 127 | 128 | algorithm = described_class.new(problem: problem) 129 | 130 | expect(algorithm.backtracking).to be_empty 131 | end 132 | end 133 | 134 | context 'when all variables are assigned' do 135 | it 'returns the assignments' do 136 | variable = double('Variable') 137 | domain = double('Domains') 138 | problem = double('Problem', variables: [variable], domains: [domain]) 139 | 140 | assignment = { variable => 1 } 141 | 142 | algorithm = described_class.new(problem: problem) 143 | 144 | expect(algorithm.backtracking(assignment)).to eq [assignment] 145 | end 146 | end 147 | 148 | context 'when using different ordering algorithm' do 149 | it 'uses the new algorithm to order the unassigned variables' do 150 | variable = double('Variable') 151 | variable2 = double('Variable') 152 | variables = [variable, variable2] 153 | 154 | domains = { variable => [1, 2], variable2 => [3, 4] } 155 | 156 | constraint = double('constraint', satisfies?: true) 157 | constraints = { variable => [constraint], variable2 => [constraint] } 158 | 159 | ordering_algorithm = double('OrderingAlgorithm') 160 | 161 | problem = double('Problem', variables: variables, domains: domains, constraints: constraints) 162 | 163 | algorithm = described_class.new(problem: problem, ordering_algorithm: ordering_algorithm) 164 | 165 | allow(algorithm).to receive(:backtracking_recursion).and_call_original 166 | allow(algorithm).to receive(:domains_for).and_call_original 167 | allow(ordering_algorithm).to receive(:call), &:reverse 168 | 169 | expect(algorithm.backtracking).to eq([{ variable => 1, variable2 => 3 }]) 170 | expect(algorithm).to have_received(:backtracking_recursion).with({}, domains) 171 | expect(algorithm).to have_received(:backtracking_recursion).with({ variable2 => 3 }, domains) 172 | expect(algorithm).to have_received(:domains_for).with(variable2, {}, domains) 173 | expect(algorithm).to have_received(:domains_for).with(variable, { variable2 => 3 }, domains) 174 | end 175 | end 176 | 177 | context 'when using different filtering algorithm' do 178 | it 'uses the new algorithm to filter the domains' do 179 | variable = double('Variable') 180 | variables = [variable] 181 | domains = { variable => [1, 2, 3, 4, 5] } 182 | constraint = double('constraint', satisfies?: false) 183 | constraints = { variable => [constraint] } 184 | filtering_algorithm = double('FilteringAlgorithm') 185 | 186 | problem = double('Problem', variables: variables, domains: domains, constraints: constraints) 187 | 188 | algorithm = described_class.new(problem: problem, filtering_algorithm: filtering_algorithm) 189 | 190 | allow(algorithm).to receive(:backtracking).and_call_original 191 | allow(algorithm).to receive(:consistent?).and_call_original 192 | allow(filtering_algorithm).to receive(:call).and_return [2, 4] 193 | 194 | expect(algorithm.backtracking).to be_empty 195 | expect(algorithm).to have_received(:backtracking).with(no_args) 196 | expect(algorithm).to have_received(:consistent?).twice 197 | expect(algorithm).to have_received(:consistent?).with(variable, { variable => 2 }) 198 | expect(algorithm).to have_received(:consistent?).with(variable, { variable => 4 }) 199 | expect(filtering_algorithm).to have_received(:call).with( 200 | values: [1, 2, 3, 4, 5], 201 | assignment_values: [] 202 | ) 203 | end 204 | end 205 | 206 | context 'when using different lookahead algorithm' do 207 | context 'when domain and lookahead are consistent' do 208 | it 'recurses with a pruned domain' do 209 | variable1 = double('Variable') 210 | variable2 = double('Variable') 211 | variables = [variable1, variable2] 212 | 213 | domains = { variable1 => [1, 2], variable2 => [1, 2] } 214 | 215 | constraint = double('constraint', variables: variables, binary?: true, unary?: false) 216 | constraints = { variable1 => [constraint], variable2 => [constraint] } 217 | 218 | problem = double('Problem', variables: variables, domains: domains, constraints: constraints) 219 | 220 | allow(constraint).to receive(:satisfies?).and_return(true, false, false, true) 221 | 222 | algorithm = described_class.new( 223 | problem: problem, 224 | lookahead_algorithm: CSP::Algorithms::Lookahead::Ac3.new(problem) 225 | ) 226 | 227 | expect(algorithm.backtracking).to eq([{ variable1 => 2, variable2 => 1 }]) 228 | end 229 | end 230 | 231 | context 'when domain is consistent but lookahead fails' do 232 | it 'does not consider value assignment' do 233 | variable1 = double('Variable') 234 | variable2 = double('Variable') 235 | variables = [variable1, variable2] 236 | 237 | domains = { variable1 => [1, 2], variable2 => [1, 2] } 238 | 239 | constraint = double('constraint', variables: variables, binary?: true, unary?: false) 240 | constraints = { variable1 => [constraint], variable2 => [constraint] } 241 | 242 | problem = double('Problem', variables: variables, domains: domains, constraints: constraints) 243 | 244 | allow(constraint).to receive(:satisfies?).and_return(true, false) 245 | 246 | algorithm = described_class.new( 247 | problem: problem, 248 | lookahead_algorithm: CSP::Algorithms::Lookahead::Ac3.new(problem) 249 | ) 250 | 251 | expect(algorithm.backtracking).to be_empty 252 | end 253 | end 254 | end 255 | end 256 | 257 | describe '#consistent?' do 258 | context 'variable satisfies all its constraints' do 259 | it 'returns true' do 260 | variable = double('Variable') 261 | constraint = double('Constraint', satisfies?: true) 262 | assignment = { variable => 1 } 263 | 264 | problem = double( 265 | 'Problem', 266 | variables: [variable], 267 | domains: spy, 268 | constraints: { variable => [constraint] } 269 | ) 270 | 271 | consistent = described_class 272 | .new(problem: problem) 273 | .consistent?(variable, assignment) 274 | 275 | expect(consistent).to eq true 276 | end 277 | end 278 | 279 | context 'when at least one constraint is not satisfied' do 280 | it 'returns false' do 281 | variable = double('Variable') 282 | constraint = double('Constraint', satisfies?: true) 283 | constraint2 = double('Constaint', satisfies?: false) 284 | assignment = { variable => 1 } 285 | 286 | problem = double( 287 | 'Problem', 288 | variables: [variable], 289 | domains: spy, 290 | constraints: { variable => [constraint, constraint2] } 291 | ) 292 | 293 | consistent = described_class 294 | .new(problem: problem) 295 | .consistent?(variable, assignment) 296 | 297 | expect(consistent).to eq false 298 | end 299 | end 300 | end 301 | end 302 | -------------------------------------------------------------------------------- /spec/csp/problem_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe CSP::Problem do 6 | describe 'initialization' do 7 | it 'defaults to empty values and min solution' do 8 | csp = described_class.new 9 | 10 | expect(csp).to have_attributes( 11 | class: described_class, 12 | variables: [], 13 | domains: {}, 14 | constraints: {}, 15 | max_solutions: 1, 16 | ordering_algorithm: nil, 17 | filtering_algorithm: nil 18 | ) 19 | end 20 | 21 | context 'when has variables and domains matching' do 22 | it 'initialize and sets constraint for the variable' do 23 | variable = double('Variable', empty?: false) 24 | variables = [variable] 25 | domains = [1, 2, 3] 26 | constraints = { variable => [] } 27 | max_solutions = 2 28 | 29 | csp = described_class.new(max_solutions: max_solutions) 30 | .add_variable(variable, domains: domains) 31 | 32 | expect(csp).to have_attributes( 33 | class: described_class, 34 | variables: variables, 35 | domains: { variable => [1, 2, 3] }, 36 | constraints: constraints, 37 | max_solutions: max_solutions 38 | ) 39 | end 40 | context 'when has multiple variables' do 41 | it 'initializes them and sets constraints for variables' do 42 | variable = double('Variable', empty?: false) 43 | variable2 = double('Variable', empty?: false) 44 | variables = [variable, variable2] 45 | domains = [1, 2, 3] 46 | constraints = { variable => [], variable2 => [] } 47 | max_solutions = 2 48 | 49 | csp = described_class.new(max_solutions: max_solutions) 50 | .add_variable(variable, domains: domains) 51 | .add_variable(variable2, domains: domains) 52 | 53 | expect(csp).to have_attributes( 54 | class: described_class, 55 | variables: variables, 56 | domains: { variable => [1, 2, 3], variable2 => [1, 2, 3] }, 57 | constraints: constraints, 58 | max_solutions: max_solutions 59 | ) 60 | end 61 | end 62 | end 63 | end 64 | 65 | describe '#solve' do 66 | it 'returns a list of solutions' do 67 | csp = described_class.new 68 | .add_variable(double('Variable', empty?: false), domains: double('domains', empty?: false)) 69 | algorithm = instance_double( 70 | CSP::Algorithms::Backtracking, 71 | backtracking: { solution: true } 72 | ) 73 | 74 | allow(CSP::Algorithms::Backtracking) 75 | .to receive(:new) 76 | .with( 77 | problem: csp, 78 | ordering_algorithm: nil, 79 | filtering_algorithm: nil, 80 | lookahead_algorithm: nil, 81 | max_solutions: 1 82 | ) 83 | .and_return algorithm 84 | 85 | expect(csp.solve).to eq([{ solution: true }]) 86 | end 87 | 88 | it 'calls backtracking without assignment' do 89 | csp = described_class.new 90 | .add_variable(double('Variable', empty?: false), domains: double('domains', empty?: false)) 91 | algorithm = instance_double( 92 | CSP::Algorithms::Backtracking, 93 | backtracking: { solution: true } 94 | ) 95 | 96 | allow(CSP::Algorithms::Backtracking) 97 | .to receive(:new) 98 | .with( 99 | problem: csp, 100 | ordering_algorithm: nil, 101 | filtering_algorithm: nil, 102 | lookahead_algorithm: nil, 103 | max_solutions: 1 104 | ) 105 | .and_return algorithm 106 | 107 | csp.solve 108 | 109 | expect(algorithm).to have_received(:backtracking).with({}) 110 | end 111 | 112 | it 'returns solutions without nil values' do 113 | csp = described_class.new 114 | .add_variable(double('Variable', empty?: false), domains: double('domains', empty?: false)) 115 | algorithm = instance_double( 116 | CSP::Algorithms::Backtracking, 117 | backtracking: nil 118 | ) 119 | 120 | allow(CSP::Algorithms::Backtracking) 121 | .to receive(:new) 122 | .with( 123 | problem: csp, 124 | ordering_algorithm: nil, 125 | filtering_algorithm: nil, 126 | lookahead_algorithm: nil, 127 | max_solutions: 1 128 | ) 129 | .and_return algorithm 130 | 131 | expect(csp.solve).to be_empty 132 | expect(algorithm).to have_received(:backtracking).with({}) 133 | end 134 | end 135 | 136 | describe '#add_variable' do 137 | it 'adds a variable to the problem' do 138 | variable = double('Variable', empty?: false) 139 | domains = double('Domains', empty?: false) 140 | variables = [variable] 141 | 142 | csp = described_class.new 143 | .add_variable(variable, domains: domains) 144 | 145 | expect(csp).to have_attributes( 146 | class: described_class, 147 | variables: variables, 148 | domains: { variable => domains }, 149 | constraints: { variable => [] }, 150 | max_solutions: 1 151 | ) 152 | end 153 | 154 | context 'when multiple variables' do 155 | it 'add all the variables' do 156 | variable = double('Variable', empty?: false) 157 | variable2 = double('Variable', empty?: false) 158 | domains = double('Domains', empty?: false) 159 | variables = [variable, variable2] 160 | 161 | csp = described_class.new 162 | .add_variable(variable, domains: domains) 163 | .add_variable(variable2, domains: domains) 164 | 165 | expect(csp).to have_attributes( 166 | class: described_class, 167 | variables: variables, 168 | domains: { variable => domains, variable2 => domains }, 169 | constraints: { variable => [], variable2 => [] }, 170 | max_solutions: 1 171 | ) 172 | end 173 | end 174 | 175 | describe '#add_variables' do 176 | it 'should call #add_variable n times' do 177 | variable = double('Variable', empty?: false) 178 | variable2 = double('Variable', empty?: false) 179 | domains = double('Domains', empty?: false) 180 | variables = [variable, variable2] 181 | 182 | csp = described_class.new 183 | allow(csp).to receive(:add_variable).and_call_original 184 | csp.add_variables(variables, domains: domains) 185 | 186 | variables.each do |variable| # rubocop:disable Lint/ShadowingOuterLocalVariable 187 | expect(csp).to have_received(:add_variable).with(variable, domains: domains).once 188 | end 189 | end 190 | 191 | it 'add multiple variables with same domain' do 192 | variable = double('Variable', empty?: false) 193 | variable2 = double('Variable', empty?: false) 194 | domains = double('Domains', empty?: false) 195 | variables = [variable, variable2] 196 | 197 | csp = described_class.new 198 | .add_variables(variables, domains: domains) 199 | 200 | expect(csp).to have_attributes( 201 | class: described_class, 202 | variables: variables, 203 | domains: { variable => domains, variable2 => domains }, 204 | constraints: { variable => [], variable2 => [] }, 205 | max_solutions: 1 206 | ) 207 | end 208 | end 209 | 210 | context 'raises an error' do 211 | it 'because variable was empty' do 212 | variable = double('Variable', empty?: true) 213 | domains = double('Domains', empty?: false) 214 | 215 | csp = described_class.new 216 | expect { csp.add_variable(variable, domains: domains) } 217 | .to raise_error described_class::VariableShouldNotBeEmpty, 218 | 'Variable was empty in the function parameter' 219 | end 220 | 221 | it 'because domains was empty' do 222 | variable = double('Variable', empty?: false) 223 | domains = double('Domains', empty?: true) 224 | 225 | csp = described_class.new 226 | expect { csp.add_variable(variable, domains: domains) } 227 | .to raise_error described_class::DomainsShouldNotBeEmpty, 228 | 'Domains was empty in the function parameter' 229 | end 230 | 231 | it 'because variable is already seted' do 232 | variable = double('Variable', empty?: false) 233 | domains = double('Domains', empty?: false) 234 | 235 | csp = described_class.new 236 | .add_variable(variable, domains: domains) 237 | 238 | expect { csp.add_variable(variable, domains: domains) } 239 | .to raise_error described_class::VariableAlreadySeted, 240 | 'Variable #[Double "Variable"] has already been seted' 241 | end 242 | end 243 | end 244 | 245 | describe '#all_different' do 246 | it 'adds the default constraint' do 247 | variable = double('Variable', empty?: false) 248 | variable2 = double('Variable', empty?: false) 249 | variables = [variable, variable2] 250 | domains = double('Domains', empty?: false) 251 | 252 | csp = described_class.new 253 | .add_variables(variables, domains: domains) 254 | .all_different 255 | 256 | expect(csp.constraints[variable].first).to be_a(CSP::Constraints::AllDifferentConstraint) 257 | expect(csp.constraints[variable2].first).to be_a(CSP::Constraints::AllDifferentConstraint) 258 | expect(csp.constraints[variable].first.variables).to match_array(variables) 259 | end 260 | end 261 | 262 | describe '#unique' do 263 | it 'adds the default constraint' do 264 | variable = double('Variable', empty?: false) 265 | variable2 = double('Variable', empty?: false) 266 | variable3 = double('Variable', empty?: false) 267 | variables = [variable, variable2, variable3] 268 | domains = double('Domains', empty?: false) 269 | 270 | csp = described_class.new 271 | .add_variables(variables, domains: domains) 272 | .unique([variable, variable2]) 273 | 274 | expect(csp.constraints[variable].first).to be_a(CSP::Constraints::UniqueConstraint) 275 | expect(csp.constraints[variable2].first).to be_a(CSP::Constraints::UniqueConstraint) 276 | expect(csp.constraints[variable3]).to eq [] 277 | expect(csp.constraints[variable].first.variables).to match_array([variable, variable2]) 278 | end 279 | end 280 | 281 | describe '#add_constraint' do 282 | context 'when passing a constraint instance to the method' do 283 | it 'adds a constraint to variable' do 284 | variable = double('Variable', empty?: false) 285 | domains = double('Domains', empty?: false) 286 | constraint = double('Constraint', variables: [variable]) 287 | 288 | csp = described_class.new 289 | .add_variable(variable, domains: domains) 290 | .add_constraint(constraint) 291 | 292 | expect(csp.constraints).to include(variable => [constraint]) 293 | end 294 | 295 | context 'when multiple variables' do 296 | it 'maps the constraint for each one' do 297 | variable = double('Variable', empty?: false) 298 | variable2 = double('Variable', empty?: false) 299 | variables = [variable, variable2] 300 | domains = double('Domains', empty?: false) 301 | constraint = double('Constraint', variables: variables) 302 | 303 | csp = described_class.new 304 | .add_variable(variable, domains: domains) 305 | .add_variable(variable2, domains: domains) 306 | .add_constraint(constraint) 307 | 308 | expect(csp.constraints).to include(variable => [constraint]) 309 | expect(csp.constraints).to include(variable2 => [constraint]) 310 | end 311 | end 312 | end 313 | 314 | context 'when passing a block and variables to the method' do 315 | it 'adds a constraint to variable' do 316 | variable = double('Variable', empty?: false) 317 | domains = double('Domains', empty?: false) 318 | variables = [variable] 319 | 320 | csp = described_class.new 321 | .add_variable(variable, domains: domains) 322 | 323 | csp.add_constraint(variables: variables) { |var| var == 0 } 324 | 325 | expect(csp.constraints[variable].first).to be_a(CSP::Constraints::CustomConstraint) 326 | end 327 | 328 | context 'when multiple variables' do 329 | it 'adds a constraint for each variable' do 330 | variable = double('Variable', empty?: false) 331 | variable2 = double('Variable', empty?: false) 332 | domains = double('Domains', empty?: false) 333 | variables = [variable, variable2] 334 | 335 | csp = described_class.new 336 | .add_variables(variables, domains: domains) 337 | 338 | csp.add_constraint(variables: variables) { |var, var3| var == var3 } 339 | 340 | expect(csp.constraints[variable].first).to be_a(CSP::Constraints::CustomConstraint) 341 | expect(csp.constraints[variable2].first).to be_a(CSP::Constraints::CustomConstraint) 342 | end 343 | end 344 | end 345 | 346 | context 'when parameter validates fail' do 347 | context 'variables was passed but block not' do 348 | it 'raises an error' do 349 | variable = double('Variable', empty?: false) 350 | domains = double('Domains', empty?: false) 351 | variables = [variable] 352 | csp = described_class.new 353 | .add_variables(variables, domains: domains) 354 | 355 | expect { csp.add_constraint(variables: variables) } 356 | .to raise_error ArgumentError, 'Either constraint or block must be provided' 357 | end 358 | end 359 | context 'block was passed but variable not' do 360 | it 'raises an error' do 361 | variable = double('Variable', empty?: false) 362 | domains = double('Domains', empty?: false) 363 | variables = [variable] 364 | 365 | csp = described_class.new 366 | .add_variables(variables, domains: domains) 367 | 368 | expect { csp.add_constraint { |var, var3| var == var3 } } 369 | .to raise_error ArgumentError, 'Variables must be provided when using a block' 370 | end 371 | end 372 | context 'constraint and block was passed' do 373 | it 'raises an error' do 374 | variable = double('Variable', empty?: false) 375 | domains = double('Domains', empty?: false) 376 | variables = [variable] 377 | constraint = double('Constraint', variables: variables) 378 | 379 | csp = described_class.new 380 | .add_variables(variables, domains: domains) 381 | 382 | expect { csp.add_constraint(constraint) { |var, var3| var == var3 } } 383 | .to raise_error ArgumentError, 'Both constraint and block cannot be provided at the same time' 384 | end 385 | end 386 | context 'block arity is greather than variables quantity' do 387 | it 'raises an error' do 388 | variable = double('Variable', empty?: false) 389 | domains = double('Domains', empty?: false) 390 | variables = [variable] 391 | 392 | csp = described_class.new 393 | .add_variables(variables, domains: domains) 394 | 395 | expect { csp.add_constraint(variables: variables) { |var, var3| var == var3 } } 396 | .to raise_error ArgumentError, 'Block should not have more arity than the quantity of variables' 397 | end 398 | end 399 | end 400 | 401 | context 'when constraint has a variable that does not exists in CSP' do 402 | it 'raises an error' do 403 | variable = double('Variable', empty?: false) 404 | diff_variable = double('Variable', empty?: false) 405 | domains = double('Domains', empty?: false) 406 | constraint = double('Constraint', variables: [variable]) 407 | 408 | csp = described_class.new 409 | .add_variable(diff_variable, domains: domains) 410 | 411 | expect { csp.add_constraint(constraint) } 412 | .to raise_error described_class::InvalidConstraintVariable, 413 | "Constraint's variable doesn't exists in CSP" 414 | end 415 | end 416 | end 417 | 418 | describe '#add_ordering' do 419 | it 'sets the ordering algorithm for CSP' do 420 | ordering_algorithm = instance_double(CSP::Algorithms::Ordering::NoOrder) 421 | csp = described_class.new 422 | .add_ordering(ordering_algorithm) 423 | 424 | expect(csp).to have_attributes(ordering_algorithm: ordering_algorithm) 425 | end 426 | end 427 | 428 | describe '#add_filtering' do 429 | it 'sets the filtering algorithm for CSP' do 430 | filtering_algorithm = instance_double(CSP::Algorithms::Filtering::NoFilter) 431 | csp = described_class.new 432 | .add_filtering(filtering_algorithm) 433 | 434 | expect(csp).to have_attributes(filtering_algorithm: filtering_algorithm) 435 | end 436 | end 437 | 438 | describe '#add_lookahead' do 439 | it 'sets the lookahead algorithm for CSP' do 440 | lookahead_algorithm = instance_double(CSP::Algorithms::Lookahead::Ac3) 441 | csp = described_class.new 442 | .add_lookahead(lookahead_algorithm) 443 | 444 | expect(csp).to have_attributes(lookahead_algorithm: lookahead_algorithm) 445 | end 446 | end 447 | end 448 | --------------------------------------------------------------------------------