logigram 0.1.0.pre
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/.rspec +2 -0
- data/.travis.yml +5 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +33 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/example.rb +35 -0
- data/lib/logigram/base.rb +273 -0
- data/lib/logigram/challenge.rb +132 -0
- data/lib/logigram/constraint.rb +74 -0
- data/lib/logigram/formatter/conjugations.rb +15 -0
- data/lib/logigram/formatter.rb +67 -0
- data/lib/logigram/piece.rb +38 -0
- data/lib/logigram/premise.rb +111 -0
- data/lib/logigram/statistics.rb +54 -0
- data/lib/logigram/version.rb +5 -0
- data/lib/logigram.rb +8 -0
- data/logigram.gemspec +36 -0
- data/test.rb +42 -0
- metadata +107 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 1873a940545e833150f3eb22b6208ec10bc122a62c38619e733b90f44b64d3c8
|
4
|
+
data.tar.gz: 11ecd0546c63ce5564abcafd1e381673b9ef0d4c593579d49cded2fa87f9ce2b
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: fdad315c96239708eac672fd6b269653d19ba897b9fd388c872377c8d1cf8fc3fd861ca37bd632378cbc942a4099ffaff3ffc4792cc92b4d9d7101dd42e66f36
|
7
|
+
data.tar.gz: e9242ac2766a70d47d838a07bd4658ea94b3c12b1a3d1f6ee941a8046c746e3187d6e748fba307103551c0c5bca4a85871dc9573b74331384fac656e5fb1d4cd
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2017 Fred Snyder
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# Logigram
|
2
|
+
|
3
|
+
A library for generating [logic puzzles](https://en.wikipedia.org/wiki/Logic_puzzle).
|
4
|
+
|
5
|
+
A Logigram puzzle is a form of *syllogism*. The puzzle provides a collection of facts (or *premises*) and asks a question. The answer can be construed from the facts through deductive reasoning.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
gem 'logigram'
|
13
|
+
```
|
14
|
+
|
15
|
+
And then execute:
|
16
|
+
|
17
|
+
$ bundle
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
|
21
|
+
$ gem install logigram
|
22
|
+
|
23
|
+
## Usage
|
24
|
+
|
25
|
+
See [example.rb](example.rb) for a simple demonstration.
|
26
|
+
|
27
|
+
## TODO:
|
28
|
+
|
29
|
+
- Dynamically generate constraints and pieces from objects with existing properties.
|
30
|
+
- Example: Three objects have existing properties `:hair_color` and `:eye_color`. The logigram uses the existing constraints
|
31
|
+
and values.
|
32
|
+
- The logigram will need to verify that the resulting challenge has exactly one discoverable solution.
|
33
|
+
- The process for generating the logigram will need a mechanism to accept constraint properties (e.g., `predicate`)
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "logigram"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/example.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
# A simple example of a Logigram puzzle
|
2
|
+
|
3
|
+
require 'logigram'
|
4
|
+
|
5
|
+
class Puzzle < Logigram::Base
|
6
|
+
# Create a formatter that refers to pieces as "animals" instead of "things"
|
7
|
+
formatter = Logigram::Formatter.new(subject: 'the %<value>s animal')
|
8
|
+
|
9
|
+
# Apply constraints that will be used to generate the puzzle's premises
|
10
|
+
constrain 'color', ['red', 'green', 'blue'], formatter: formatter
|
11
|
+
constrain 'size', ['small', 'medium', 'large'], formatter: formatter
|
12
|
+
end
|
13
|
+
|
14
|
+
# Create a new puzzle with three pieces. If a `solution` is not specified, the
|
15
|
+
# puzzle will select one at random
|
16
|
+
puzzle = Puzzle.new(['the dog', 'the cat', 'the pig'])
|
17
|
+
|
18
|
+
# The challenge holds the clues the player can use to solve the puzzle
|
19
|
+
challenge = Logigram::Challenge.new(puzzle)
|
20
|
+
|
21
|
+
puts "The animals are #{puzzle.pieces.join(', ')}"
|
22
|
+
puzzle.constraints.each do |c|
|
23
|
+
puts "One of each is #{c.values.join(', ')}"
|
24
|
+
end
|
25
|
+
puts "Which animal #{puzzle.solution_predicate}?"
|
26
|
+
|
27
|
+
puts 'Known facts:'
|
28
|
+
challenge.clues.each do |c|
|
29
|
+
puts "* #{c.to_s.capitalize}"
|
30
|
+
end
|
31
|
+
|
32
|
+
print 'Press enter for the solution....'
|
33
|
+
$stdin.gets
|
34
|
+
|
35
|
+
puts "#{puzzle.solution.to_s.capitalize} #{puzzle.solution_predicate}."
|
@@ -0,0 +1,273 @@
|
|
1
|
+
module Logigram
|
2
|
+
# A base class for creating logic puzzles. Authors should not instantiate
|
3
|
+
# this class directly, but extend it with their own puzzle implementations.
|
4
|
+
#
|
5
|
+
# @example
|
6
|
+
# class Puzzle < Logigram::Base
|
7
|
+
# constrain 'color', ['red', 'green', 'blue']
|
8
|
+
# constrain 'size', ['small', 'medium', 'large']
|
9
|
+
# end
|
10
|
+
#
|
11
|
+
class Base
|
12
|
+
# The piece that represents the solution. The puzzle's premises should be
|
13
|
+
# clues from which this solution can be deduced.
|
14
|
+
#
|
15
|
+
# @return [Piece]
|
16
|
+
attr_reader :solution
|
17
|
+
|
18
|
+
class << self
|
19
|
+
# An array of the puzzle's constraints.
|
20
|
+
#
|
21
|
+
# Constraints specify attributes that get assigned to puzzle pieces.
|
22
|
+
# When a puzzle gets instantiated, each piece is assigned a value from
|
23
|
+
# each constraint, and the puzzle uses them to generate premises.
|
24
|
+
#
|
25
|
+
# @return [Array<Constraint>]
|
26
|
+
def constraints
|
27
|
+
constraint_map.values
|
28
|
+
end
|
29
|
+
|
30
|
+
# Get a constraint by name.
|
31
|
+
#
|
32
|
+
# @param name [String]
|
33
|
+
# @return [Constraint, nil]
|
34
|
+
def constraint(name)
|
35
|
+
constraint_map[name]
|
36
|
+
end
|
37
|
+
|
38
|
+
protected
|
39
|
+
|
40
|
+
# Add a constraint to the puzzle.
|
41
|
+
#
|
42
|
+
# @example The pieces in the instantiated puzzle will each be assigned a unique color and size.
|
43
|
+
#
|
44
|
+
# class MyPuzzle < Logigram::Base
|
45
|
+
# constrain 'color', ['red', 'green', 'blue']
|
46
|
+
# constrain 'size', ['small', 'medium', 'large']
|
47
|
+
# end
|
48
|
+
# puzzle = MyPuzzle.new ['dog', 'cat', 'rat']
|
49
|
+
#
|
50
|
+
# @param name [String]
|
51
|
+
# @param values [Array<Object>]
|
52
|
+
# @param reserve [Object, Array<Object>, nil] Require the solution to be one of these values
|
53
|
+
# @param formatter [Formatter] Formatting rules for generated premises
|
54
|
+
# @return [Logigram::Constraint] The newly created constraint
|
55
|
+
def constrain(name, values, reserve: nil, formatter: Formatter::DEFAULT)
|
56
|
+
constraint_map[name] = Constraint.new(name, values, reserve: reserve, formatter: formatter)
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
# @return [Hash<String, Constraint>]
|
62
|
+
def constraint_map
|
63
|
+
@constraint_map ||= {}
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Generate a puzzle with the provided configuration.
|
68
|
+
#
|
69
|
+
# The `objects` array is required. If `solution` and `terms` are not provided, they'll be randomly generated.
|
70
|
+
#
|
71
|
+
# @param objects [Array<Object>] The piece identifiers
|
72
|
+
# @param solution [Object] Which object to use as the solution
|
73
|
+
# @param terms [String, Array<String>, nil] The solution term(s)
|
74
|
+
# @param recur [String, Array<String>, nil] Recurring constraints (uniqueness never enforced)
|
75
|
+
def initialize(objects, solution: objects.sample, terms: nil, recur: nil)
|
76
|
+
@object_pieces = {}
|
77
|
+
@solution_terms = terms ? [terms].flatten : [constraints.map(&:name).sample]
|
78
|
+
@recur = recur ? [recur].flatten : []
|
79
|
+
objects.push solution unless objects.include?(solution)
|
80
|
+
generate_pieces objects, solution
|
81
|
+
end
|
82
|
+
|
83
|
+
# @return [Array<Premise>]
|
84
|
+
def premises
|
85
|
+
@premises ||= generate_all_premises
|
86
|
+
end
|
87
|
+
|
88
|
+
# @return [Array<Constraint>]
|
89
|
+
def constraints
|
90
|
+
self.class.constraints
|
91
|
+
end
|
92
|
+
|
93
|
+
# @param name [String]
|
94
|
+
# @return [Constraint, nil]
|
95
|
+
def constraint(name)
|
96
|
+
self.class.constraint name
|
97
|
+
end
|
98
|
+
|
99
|
+
# Get an array of values for a constraint.
|
100
|
+
# This method will only include values that are currently assigned to pieces.
|
101
|
+
#
|
102
|
+
# @return [Array<Object>]
|
103
|
+
def term_values(key)
|
104
|
+
# Use an intersection to retain the order in which the values were
|
105
|
+
# assigned to the constraint
|
106
|
+
constraint(key).values & pieces.map { |piece| piece.value(key) }
|
107
|
+
end
|
108
|
+
|
109
|
+
# The terms that should be used to identify the solution.
|
110
|
+
#
|
111
|
+
# @return [Array<String>]
|
112
|
+
attr_reader :solution_terms
|
113
|
+
|
114
|
+
def solution_term
|
115
|
+
raise 'Use `solution_terms` when there is more than one term' unless @solution_terms.one?
|
116
|
+
|
117
|
+
@solution_terms.first
|
118
|
+
end
|
119
|
+
|
120
|
+
# Shortcut to get the solution terms' values, e.g., "red"
|
121
|
+
#
|
122
|
+
# @return [Array<String>]
|
123
|
+
def solution_values
|
124
|
+
@solution_terms.map { |t| @solution.value(t) }
|
125
|
+
end
|
126
|
+
|
127
|
+
# Shortcut to get the solution term's value, e.g., "red"
|
128
|
+
#
|
129
|
+
# @raise [RuntimeError] if there is more than one solution term
|
130
|
+
# @return [String]
|
131
|
+
def solution_value
|
132
|
+
raise 'Use `solution_values` when there is more than one term' unless @solution_terms.one?
|
133
|
+
|
134
|
+
@solution.value(@solution_terms.first)
|
135
|
+
end
|
136
|
+
|
137
|
+
# Shortcut to get the solution terms' predicates, e.g., "is red"
|
138
|
+
#
|
139
|
+
# @return [Array<String>]
|
140
|
+
def solution_predicates
|
141
|
+
@solution_terms.map { |t| constraint(t).predicate(@solution.value(t)) }
|
142
|
+
end
|
143
|
+
|
144
|
+
# Shortcut to get the solution term's predicate, e.g., "is red"
|
145
|
+
#
|
146
|
+
# @return [String]
|
147
|
+
def solution_predicate
|
148
|
+
raise 'Use `solution_predicates` when there is more than one term' unless @solution_terms.one?
|
149
|
+
|
150
|
+
constraint(@solution_terms.first).predicate(@solution.value(@solution_terms.first))
|
151
|
+
end
|
152
|
+
|
153
|
+
# @return [Array<Logigram::Piece>]
|
154
|
+
def pieces
|
155
|
+
@object_pieces.values
|
156
|
+
end
|
157
|
+
|
158
|
+
# Get the piece associated with an object.
|
159
|
+
#
|
160
|
+
# @param object [Object] The object used to generate the piece
|
161
|
+
# @return [Logigram::Piece]
|
162
|
+
def piece_for(object)
|
163
|
+
@object_pieces[object]
|
164
|
+
end
|
165
|
+
|
166
|
+
private
|
167
|
+
|
168
|
+
# Generate the puzzle pieces.
|
169
|
+
#
|
170
|
+
# @param objects [Array<Object>]
|
171
|
+
# @param solution [Object]
|
172
|
+
# @return [void]
|
173
|
+
def generate_pieces(objects, solution)
|
174
|
+
selected_values = {}
|
175
|
+
solution_terms.each do |term|
|
176
|
+
selected_values[term] = constraint(term).reserves.sample
|
177
|
+
end
|
178
|
+
@solution = generate_piece(solution, selected_values, true)
|
179
|
+
objects.each do |o|
|
180
|
+
@object_pieces[o] = if o == solution
|
181
|
+
@solution
|
182
|
+
else
|
183
|
+
generate_piece(o, selected_values, false)
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
# @param object [Object]
|
189
|
+
# @param selected_values [Hash]
|
190
|
+
# @param selected [Boolean]
|
191
|
+
# @return [Piece]
|
192
|
+
def generate_piece(object, selected_values, selected)
|
193
|
+
constraint_repo = generate_constraint_repo(selected_values, selected)
|
194
|
+
terms = {}
|
195
|
+
constraint_repo.each_pair do |key, values|
|
196
|
+
raise "Unable to select value for constraint '#{key}'" if values.empty?
|
197
|
+
|
198
|
+
terms[key] = values.sample
|
199
|
+
end
|
200
|
+
Piece.new(object, terms)
|
201
|
+
end
|
202
|
+
|
203
|
+
# @param selected_values [Hash]
|
204
|
+
# @param selected [Boolean]
|
205
|
+
# @return [Hash]
|
206
|
+
def generate_constraint_repo(selected_values, selected)
|
207
|
+
repo = {}
|
208
|
+
# Setting a fixed term ensures that at least one solution term will not
|
209
|
+
# be duplicated by another piece
|
210
|
+
fixed_term = selected_values.keys.sample
|
211
|
+
constraints.each do |c|
|
212
|
+
repo[c.name] = if selected_values.key?(c.name)
|
213
|
+
if selected
|
214
|
+
[selected_values[c.name]]
|
215
|
+
elsif c.name == fixed_term
|
216
|
+
limit_available_values(c, selected_values[fixed_term], selected)
|
217
|
+
else
|
218
|
+
limit_available_values(c, nil, false)
|
219
|
+
end
|
220
|
+
else
|
221
|
+
limit_available_values(c, nil, selected)
|
222
|
+
end
|
223
|
+
end
|
224
|
+
repo
|
225
|
+
end
|
226
|
+
|
227
|
+
# @param constraint [Constraint]
|
228
|
+
# @param exception [String]
|
229
|
+
# @param selected [Boolean]
|
230
|
+
# @return [Array<String>]
|
231
|
+
def limit_available_values(constraint, exception, selected)
|
232
|
+
available = (selected ? constraint.reserves : constraint.values) - [exception]
|
233
|
+
filtered = available -
|
234
|
+
[@solution&.value(constraint.name)] -
|
235
|
+
(@recur.include?(constraint.name) ? [] : pieces.map { |p| p.value(constraint.name) })
|
236
|
+
filtered.empty? ? available : filtered
|
237
|
+
end
|
238
|
+
|
239
|
+
# Create an array of all possible premises for the puzzle.
|
240
|
+
#
|
241
|
+
# @return [Array<Logigram::Premise>]
|
242
|
+
def generate_all_premises
|
243
|
+
pieces.map { |pc| generate_piece_premises pc }.flatten
|
244
|
+
end
|
245
|
+
|
246
|
+
# Create an array of all possible premises for the specified piece.
|
247
|
+
#
|
248
|
+
# @param piece [Logigram::Piece]
|
249
|
+
# @return [Array<Logigram::Premise>]
|
250
|
+
def generate_piece_premises(piece)
|
251
|
+
result = []
|
252
|
+
piece.terms.each do |t|
|
253
|
+
# Positive specific
|
254
|
+
result.push Premise.new(piece, constraint(t), piece.value(t))
|
255
|
+
# Positive generic
|
256
|
+
(constraints - [constraint(t)]).each do |o|
|
257
|
+
result.push Premise.new(piece, constraint(t), piece.value(t), o)
|
258
|
+
end
|
259
|
+
# Negative specific
|
260
|
+
(term_values(t) - [piece.value(t)]).each do |o|
|
261
|
+
result.push Premise.new(piece, constraint(t), o)
|
262
|
+
end
|
263
|
+
# Negative generic
|
264
|
+
(term_values(t) - [piece.value(t)]).each do |o|
|
265
|
+
(constraints - [constraint(t)]).each do |id|
|
266
|
+
result.push Premise.new(piece, constraint(t), o, id)
|
267
|
+
end
|
268
|
+
end
|
269
|
+
end
|
270
|
+
result
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
module Logigram
|
2
|
+
# Use the Logigram::Challenge class to generate a list of clues from a
|
3
|
+
# puzzle.
|
4
|
+
#
|
5
|
+
# Challenges have three degrees of difficulty.
|
6
|
+
# - easy: all affirmative premises
|
7
|
+
# - medium: mixture of affirmative and negative premises
|
8
|
+
# - hard: one affirmative premise per constraint
|
9
|
+
#
|
10
|
+
class Challenge
|
11
|
+
# @return [Array<Logigram::Premise>]
|
12
|
+
attr_reader :clues
|
13
|
+
|
14
|
+
attr_reader :puzzle
|
15
|
+
|
16
|
+
# @param puzzle [Logigram::Base]
|
17
|
+
# @param difficulty [Symbol] :easy, :medium, :hard
|
18
|
+
def initialize(puzzle, difficulty: :medium)
|
19
|
+
@puzzle = puzzle
|
20
|
+
@clues = []
|
21
|
+
@term_values = {}
|
22
|
+
@difficulty = difficulty
|
23
|
+
generate_premises
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def solution_constraints
|
29
|
+
@solution_constraints ||= @puzzle.solution_terms.map { |t| @puzzle.constraint(t) }
|
30
|
+
end
|
31
|
+
|
32
|
+
# @return [Array<Constraint>]
|
33
|
+
def unique_constraints
|
34
|
+
@unique_constraints ||= solution_constraints.select do |con|
|
35
|
+
value = @puzzle.solution.value(con.name)
|
36
|
+
@puzzle.pieces.select { |p| p.value(con.name) == value }.one?
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# @return [Array<Constraint>]
|
41
|
+
def sorted_constraints
|
42
|
+
@sorted_constraints ||= begin
|
43
|
+
fixed_constraints = unique_constraints || [solution_constraints.sample]
|
44
|
+
other = (@puzzle.constraints - fixed_constraints).shuffle
|
45
|
+
first = other.shift
|
46
|
+
(first ? [first] : []) + other + fixed_constraints
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Remove a value from a term's availability list.
|
51
|
+
#
|
52
|
+
# @param term [String]
|
53
|
+
# @param value [String]
|
54
|
+
# @return [void]
|
55
|
+
def remove_value(term, value)
|
56
|
+
@term_values[term] ||= @puzzle.term_values(term)
|
57
|
+
@term_values[term].delete value
|
58
|
+
end
|
59
|
+
|
60
|
+
# Get a random value from a term's availability list.
|
61
|
+
#
|
62
|
+
# @param term [String]
|
63
|
+
# @param except [String, nil]
|
64
|
+
def sample_value(term, except: nil)
|
65
|
+
@term_values[term] ||= @puzzle.term_values(term)
|
66
|
+
# Try to eliminate the exception but allow it if it's the only option
|
67
|
+
(@term_values[term] - [except]).sample || @term_values[term].sample
|
68
|
+
end
|
69
|
+
|
70
|
+
# @return [void]
|
71
|
+
def generate_premises
|
72
|
+
last_constraint = nil
|
73
|
+
sorted_constraints[0..-2].each do |constraint|
|
74
|
+
shuffled_pieces = @puzzle.pieces.shuffle
|
75
|
+
shuffled_pieces[0..-2].each_with_index do |piece, index|
|
76
|
+
@clues.push generate_premise(piece, constraint, last_constraint, affirmation_at(index))
|
77
|
+
end
|
78
|
+
last_constraint = constraint
|
79
|
+
end
|
80
|
+
(@puzzle.pieces - [@puzzle.solution]).shuffle.each_with_index do |piece, index|
|
81
|
+
@clues.push generate_premise(piece, sorted_constraints.last, last_constraint, affirmation_at(index))
|
82
|
+
end
|
83
|
+
@clues = [@clues[0]] + @clues[1..-1].shuffle
|
84
|
+
end
|
85
|
+
|
86
|
+
# @param index [Integer]
|
87
|
+
# @return [Symbol]
|
88
|
+
def affirmation_at(index)
|
89
|
+
if @difficulty == :easy
|
90
|
+
:affirmative
|
91
|
+
elsif @difficulty == :medium
|
92
|
+
if index < @puzzle.pieces.length - 2
|
93
|
+
:affirmative
|
94
|
+
else
|
95
|
+
:random
|
96
|
+
end
|
97
|
+
elsif @difficulty == :hard
|
98
|
+
if index == 0
|
99
|
+
:affirmative
|
100
|
+
else
|
101
|
+
:negative
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# @param piece [Logigram::Piece]
|
107
|
+
# @param constraint [Logigram::Constraint]
|
108
|
+
# @param identifier [Logigram::Constraint]
|
109
|
+
# @param affirm [Symbol] :affirmative, :negative, :random
|
110
|
+
def generate_premise(piece, constraint, identifier, affirm)
|
111
|
+
value = case affirm
|
112
|
+
when :affirmative
|
113
|
+
piece.value(constraint.name)
|
114
|
+
when :negative
|
115
|
+
sample_value(constraint.name, except: piece.value(constraint.name))
|
116
|
+
else
|
117
|
+
sample_value(constraint.name)
|
118
|
+
end
|
119
|
+
remove_value constraint.name, value
|
120
|
+
Logigram::Premise.new(piece, constraint, value, clarify(piece, identifier))
|
121
|
+
end
|
122
|
+
|
123
|
+
# @param piece [Piece]
|
124
|
+
# @param identifier [Constraint, nil]
|
125
|
+
def clarify(piece, identifier)
|
126
|
+
return nil unless identifier
|
127
|
+
|
128
|
+
total = @puzzle.pieces.select { |p| p.value(identifier.name) == piece.value(identifier.name) }
|
129
|
+
total.length == 1 ? identifier : nil
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module Logigram
|
2
|
+
# Constraints describe features that puzzle pieces can possess. They are
|
3
|
+
# identified by name and have a finite set of possible values. When a puzzle
|
4
|
+
# is generated, its pieces are assigned a unique random value for each
|
5
|
+
# constraint.
|
6
|
+
#
|
7
|
+
class Constraint
|
8
|
+
# @return [String]
|
9
|
+
attr_reader :name
|
10
|
+
|
11
|
+
# All possible values for the constraint
|
12
|
+
#
|
13
|
+
# @return [Array<String>]
|
14
|
+
attr_reader :values
|
15
|
+
|
16
|
+
# The subset of values that can be applied to solutions. When a puzzle
|
17
|
+
# generates a solution, its value for the constraint should be one of the
|
18
|
+
# reserves. (By default, reserves are all possible values.)
|
19
|
+
#
|
20
|
+
# @return [Array<String>]
|
21
|
+
attr_reader :reserves
|
22
|
+
|
23
|
+
attr_reader :formatter
|
24
|
+
|
25
|
+
# @param name [String]
|
26
|
+
# @param values [Array] All possible values for the constraint
|
27
|
+
# @param reserve [Object, Array<Object>, nil] Values to reserve for solutions
|
28
|
+
# @param formatter [Formatter] Formatting rules
|
29
|
+
def initialize name, values, reserve: nil, formatter: Formatter::DEFAULT
|
30
|
+
@name = name
|
31
|
+
@values = values
|
32
|
+
@reserves = configure_reserves(reserve)
|
33
|
+
@formatter = formatter
|
34
|
+
end
|
35
|
+
|
36
|
+
# A noun form for the value, e.g., "the red thing"
|
37
|
+
#
|
38
|
+
# @return [String]
|
39
|
+
def subject value
|
40
|
+
validate value
|
41
|
+
formatter.subject(value)
|
42
|
+
end
|
43
|
+
|
44
|
+
# A verbal predicate for the value, e.g., "is red"
|
45
|
+
#
|
46
|
+
# @return [String]
|
47
|
+
def predicate value, quantity = 1
|
48
|
+
validate value
|
49
|
+
formatter.predicate(value, quantity)
|
50
|
+
end
|
51
|
+
|
52
|
+
# A negative verbal predicate form for the value, e.g., "is not red"
|
53
|
+
def negative value, quantity = 1
|
54
|
+
validate value
|
55
|
+
formatter.negative(value, quantity)
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
# @raise [ArgumentError] if the value is not valid
|
61
|
+
# @param value [String]
|
62
|
+
# @return [String]
|
63
|
+
def validate value
|
64
|
+
return value if values.include?(value)
|
65
|
+
raise ArgumentError, "Constraint for #{name} received invalid value #{value}"
|
66
|
+
end
|
67
|
+
|
68
|
+
# @param reserve [String, Array<Sting>, nil]
|
69
|
+
def configure_reserves(reserve)
|
70
|
+
return values unless reserve
|
71
|
+
[reserve].flatten.map { |v| validate(v) }
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Logigram
|
2
|
+
class Formatter
|
3
|
+
CONJUGATIONS = {
|
4
|
+
be: ['is', 'are', 'is not', 'are not'],
|
5
|
+
drink: ['drinks', 'drink', 'does not drink', 'do not drink'],
|
6
|
+
eat: ['eats', 'eat', 'does not eat', 'do not eat'],
|
7
|
+
have: ['has', 'have', 'does not have', 'do not have'],
|
8
|
+
like: ['likes', 'like', 'does not like', 'do not like'],
|
9
|
+
live_in: ['lives in', 'live in', 'does not live in', 'do not live in'],
|
10
|
+
own: ['owns', 'own', 'does not own', 'do not own'],
|
11
|
+
play: ['plays', 'play', 'does not play', 'do not play'],
|
12
|
+
work_as: ['works as', 'work as', 'does not work as', 'do not work as']
|
13
|
+
}
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'logigram/formatter/conjugations'
|
2
|
+
|
3
|
+
module Logigram
|
4
|
+
# Rules for generating premise statements.
|
5
|
+
#
|
6
|
+
# @example Use the default rules, but refer to pieces as "objects" instead of "things"
|
7
|
+
# Formatter.new(subject: 'the %{value} object')
|
8
|
+
# # Example premise: "the green object is large"
|
9
|
+
#
|
10
|
+
# @example Use the default rules, except make the "be" conjugations past tense
|
11
|
+
# Formatter.new(verb: ['was', 'were', 'was not', 'were not'])
|
12
|
+
# # Example premise: "the red thing was small"
|
13
|
+
#
|
14
|
+
class Formatter
|
15
|
+
# @param subject [String]
|
16
|
+
# @param plural [String]
|
17
|
+
# @param verb [Symbol, Array<String>]
|
18
|
+
# @param descriptor [String]
|
19
|
+
def initialize subject: 'the %{value} thing', plural: "#{subject}s", verb: :be, descriptor: '%{value}'
|
20
|
+
@conjugations = validate_verb(verb)
|
21
|
+
@subject = subject
|
22
|
+
@plural = plural
|
23
|
+
@descriptor = descriptor
|
24
|
+
end
|
25
|
+
|
26
|
+
def subject value, amount = 1
|
27
|
+
(amount == 1 ? @subject : @plural) % {value: fix_article(value)}
|
28
|
+
end
|
29
|
+
|
30
|
+
def predicate value, amount = 1
|
31
|
+
"#{predicate_verb(amount)} #{@descriptor % {value: value}}"
|
32
|
+
end
|
33
|
+
|
34
|
+
def negative value, amount = 1
|
35
|
+
"#{negative_verb(amount)} #{@descriptor % {value: value}}"
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def predicate_verb amount
|
41
|
+
amount == 1 ? @conjugations[0] : @conjugations[1]
|
42
|
+
end
|
43
|
+
|
44
|
+
def negative_verb amount
|
45
|
+
amount == 1 ? @conjugations[2] : @conjugations[3]
|
46
|
+
end
|
47
|
+
|
48
|
+
def fix_article(value)
|
49
|
+
return value unless @subject.include?('the %{value}')
|
50
|
+
value.to_s.sub(/^(a|an) /, '')
|
51
|
+
end
|
52
|
+
|
53
|
+
# @return [Array<String>]
|
54
|
+
def validate_verb verb
|
55
|
+
CONJUGATIONS[verb] ||
|
56
|
+
validate_conjugation(verb) ||
|
57
|
+
raise(ArgumentError, 'Verb must be a predefined infinitive or an array of verb forms')
|
58
|
+
end
|
59
|
+
|
60
|
+
# @return [Array<String>]
|
61
|
+
def validate_conjugation verb
|
62
|
+
return verb if verb.is_a?(Array) && verb.length == 4
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
Formatter::DEFAULT = Formatter.new
|
67
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Logigram
|
2
|
+
class Piece
|
3
|
+
# @return [Object]
|
4
|
+
attr_reader :object
|
5
|
+
|
6
|
+
# @param object [Object]
|
7
|
+
# @param terms [Hash]
|
8
|
+
# @param name [String]
|
9
|
+
def initialize object, terms, name: nil
|
10
|
+
@object = object
|
11
|
+
@terms = terms
|
12
|
+
@name = name
|
13
|
+
end
|
14
|
+
|
15
|
+
def name
|
16
|
+
@name || object.to_s
|
17
|
+
end
|
18
|
+
|
19
|
+
# Get the value assigned to this piece for the specified term.
|
20
|
+
#
|
21
|
+
# @param term [String] The name of a constraint
|
22
|
+
# @return [Object]
|
23
|
+
def value term
|
24
|
+
@terms[term]
|
25
|
+
end
|
26
|
+
|
27
|
+
# The names of all the constraints associated with this piece.
|
28
|
+
#
|
29
|
+
# @return [Array<String>]
|
30
|
+
def terms
|
31
|
+
@terms.keys
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_s
|
35
|
+
name
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
module Logigram
|
2
|
+
# A premise is a fact about a puzzle piece. Puzzles use premises to provide
|
3
|
+
# clues.
|
4
|
+
#
|
5
|
+
# Examples of premises:
|
6
|
+
#
|
7
|
+
# "Bob is short."
|
8
|
+
# "Mary is not tall."
|
9
|
+
# "The short person has red hair."
|
10
|
+
#
|
11
|
+
class Premise
|
12
|
+
# @return [Piece]
|
13
|
+
attr_reader :piece
|
14
|
+
|
15
|
+
# @return [Constraint]
|
16
|
+
attr_reader :constraint
|
17
|
+
|
18
|
+
# @return [String]
|
19
|
+
attr_reader :value
|
20
|
+
|
21
|
+
# @return [Constraint, nil]
|
22
|
+
attr_reader :identifier
|
23
|
+
|
24
|
+
# @param piece [Piece]
|
25
|
+
# @param constraint [Constraint]
|
26
|
+
# @param value [String]
|
27
|
+
# @param identifier [Constraint, nil]
|
28
|
+
def initialize(piece, constraint, value, identifier = nil)
|
29
|
+
@piece = piece
|
30
|
+
@constraint = constraint
|
31
|
+
@identifier = identifier
|
32
|
+
@value = value
|
33
|
+
end
|
34
|
+
|
35
|
+
# Determine if this premise refers to its piece specifically or uses an
|
36
|
+
# alternate identifier.
|
37
|
+
# A specific premise uses the piece's name for the subject, e.g., "Bob."
|
38
|
+
# The subject of an alternate premise is typically a description, e.g.,
|
39
|
+
# "The gray cat" or "The person with brown hair."
|
40
|
+
#
|
41
|
+
# @return [Boolean]
|
42
|
+
def specific?
|
43
|
+
identifier.nil?
|
44
|
+
end
|
45
|
+
|
46
|
+
# Determine if this is a generic premise.
|
47
|
+
# @see #specific?
|
48
|
+
def generic?
|
49
|
+
!specific?
|
50
|
+
end
|
51
|
+
|
52
|
+
# Determine if this is an affirmative premise.
|
53
|
+
# An affirmative premise is a fact, e.g., "The dog is red."
|
54
|
+
# A negative premise is a reduction, e.g., "The dog is not blue."
|
55
|
+
#
|
56
|
+
# @return [Boolean]
|
57
|
+
def affirmative?
|
58
|
+
piece.value(constraint.name) == value
|
59
|
+
end
|
60
|
+
|
61
|
+
# Determine if this is a negative premise.
|
62
|
+
# @see #affirmative?
|
63
|
+
#
|
64
|
+
# @return [Boolean]
|
65
|
+
def negative?
|
66
|
+
!affirmative?
|
67
|
+
end
|
68
|
+
|
69
|
+
# The name of the constraint for which this premise provides a fact.
|
70
|
+
# Example: for the premise `the dog is red`, the term is `color`.
|
71
|
+
#
|
72
|
+
# @return [String]
|
73
|
+
def term
|
74
|
+
constraint.name
|
75
|
+
end
|
76
|
+
|
77
|
+
# The subject is the puzzle piece for which this premise provides a fact.
|
78
|
+
# If the premise is `specific`, the subject is the piece's name. Otherwise
|
79
|
+
# the subject is a description of the piece based on the constraint being
|
80
|
+
# used as an `identifier`.
|
81
|
+
#
|
82
|
+
# Example: The premise's piece is named Bob. The puzzle's constraints
|
83
|
+
# include hair color. Bob's hair is red. If this premise is specific, the
|
84
|
+
# subject would be `Bob`. If the premise is not specific and its identifier
|
85
|
+
# is hair color, the subject would be `the person with red hair`.
|
86
|
+
#
|
87
|
+
# @return [String]
|
88
|
+
def subject
|
89
|
+
@subject ||= if identifier.nil?
|
90
|
+
piece.name
|
91
|
+
else
|
92
|
+
identifier.subject(piece.value(identifier.name))
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# A human-readable representation of the premise, e.g., "The dog is red."
|
97
|
+
#
|
98
|
+
# @return [String]
|
99
|
+
def text
|
100
|
+
@text ||= if affirmative?
|
101
|
+
"#{subject} #{constraint.predicate(value)}"
|
102
|
+
else
|
103
|
+
"#{subject} #{constraint.negative(value)}"
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def to_s
|
108
|
+
text
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Logigram
|
2
|
+
# A data summary about the pieces and premises of a puzzle.
|
3
|
+
#
|
4
|
+
# @param puzzle [Logigram::Base]
|
5
|
+
# @param subject [String]
|
6
|
+
# @param plural [String]
|
7
|
+
class Statistics
|
8
|
+
def initialize puzzle, subject: 'thing', plural: "#{subject}s"
|
9
|
+
@puzzle = puzzle
|
10
|
+
@subject = subject
|
11
|
+
@plural = plural
|
12
|
+
end
|
13
|
+
|
14
|
+
# @return [Hash]
|
15
|
+
def raw_data
|
16
|
+
@raw_data ||= generate_statistics
|
17
|
+
end
|
18
|
+
|
19
|
+
# @return [Array<String>]
|
20
|
+
def statements
|
21
|
+
@statements ||= generate_statements
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def noun qty
|
27
|
+
qty == 1 ? @subject : @plural
|
28
|
+
end
|
29
|
+
|
30
|
+
def generate_statistics
|
31
|
+
stats = {}
|
32
|
+
@puzzle.constraints.each do |con|
|
33
|
+
values = {}
|
34
|
+
@puzzle.pieces.each do |pc|
|
35
|
+
values[pc.value(con.name)] ||= 0
|
36
|
+
values[pc.value(con.name)] += 1
|
37
|
+
end
|
38
|
+
stats[con.name] = values
|
39
|
+
end
|
40
|
+
stats
|
41
|
+
end
|
42
|
+
|
43
|
+
def generate_statements
|
44
|
+
lines = []
|
45
|
+
raw_data.each_pair do |key, values|
|
46
|
+
con = @puzzle.constraint(key)
|
47
|
+
values.each_pair do |val, qty|
|
48
|
+
lines.push "#{qty} #{noun(qty)} #{con.predicate(val, qty)}"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
lines
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
data/lib/logigram.rb
ADDED
data/logigram.gemspec
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'logigram/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "logigram"
|
8
|
+
spec.version = Logigram::VERSION
|
9
|
+
spec.authors = ["Fred Snyder"]
|
10
|
+
spec.email = ["fsnyder@castwide.com"]
|
11
|
+
|
12
|
+
spec.summary = "A library for generating logic puzzles"
|
13
|
+
#spec.description = %q{TODO: Write a longer description or delete this line.}
|
14
|
+
spec.homepage = "http://castwide.com"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
# Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
|
18
|
+
# to allow pushing to a single host or delete this section to allow pushing to any host.
|
19
|
+
# if spec.respond_to?(:metadata)
|
20
|
+
# spec.metadata['allowed_push_host'] = "TODO: Set to 'http://mygemserver.com'"
|
21
|
+
# else
|
22
|
+
# raise "RubyGems 2.0 or newer is required to protect against " \
|
23
|
+
# "public gem pushes."
|
24
|
+
# end
|
25
|
+
|
26
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
27
|
+
f.match(%r{^(test|spec|features)/})
|
28
|
+
end
|
29
|
+
spec.bindir = "exe"
|
30
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
31
|
+
spec.require_paths = ["lib"]
|
32
|
+
|
33
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
34
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
35
|
+
spec.add_development_dependency 'simplecov'
|
36
|
+
end
|
data/test.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'logigram'
|
2
|
+
|
3
|
+
class Murder < Logigram::Base
|
4
|
+
constrain 'alibi', ['true alibi', 'false alibi', 'no alibi'], reserve: ['false alibi', 'no alibi']
|
5
|
+
constrain 'weapon', ['with weapon', 'without weapon'], reserve: ['with weapon']
|
6
|
+
constrain 'motive', ['motivated', 'motiveless'], reserve: ['motivated']
|
7
|
+
constrain 'proof', ['with proof', 'without proof'], reserve: ['with proof']
|
8
|
+
constrain 'knows', ['knows suspect', 'knows address', 'knows motive', 'knows nothing']
|
9
|
+
end
|
10
|
+
|
11
|
+
class Entity
|
12
|
+
attr_reader :name
|
13
|
+
|
14
|
+
def initialize(name)
|
15
|
+
@name = name
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_s
|
19
|
+
name
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
characters = [
|
24
|
+
Entity.new('Bob'),
|
25
|
+
Entity.new('Joe'),
|
26
|
+
Entity.new('Tim'),
|
27
|
+
Entity.new('Dan'),
|
28
|
+
Entity.new('Hal')
|
29
|
+
]
|
30
|
+
|
31
|
+
puzzle = Murder.new(characters, recur: true, terms: %w[alibi weapon])
|
32
|
+
|
33
|
+
# challenge = Logigram::Challenge.new(puzzle, difficulty: :easy)
|
34
|
+
# challenge.clues.each do |clue|
|
35
|
+
# puts "#{clue.piece.name}, #{clue.value}"
|
36
|
+
# end
|
37
|
+
# puts challenge.puzzle.solution
|
38
|
+
|
39
|
+
puzzle.premises.select(&:affirmative?).select(&:specific?).each do |premise|
|
40
|
+
puts "#{premise.piece}, #{premise.value}"
|
41
|
+
end
|
42
|
+
puts puzzle.solution
|
metadata
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: logigram
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0.pre
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Fred Snyder
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2025-05-10 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rake
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '10.0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '10.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rspec
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '3.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '3.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: simplecov
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
description:
|
56
|
+
email:
|
57
|
+
- fsnyder@castwide.com
|
58
|
+
executables: []
|
59
|
+
extensions: []
|
60
|
+
extra_rdoc_files: []
|
61
|
+
files:
|
62
|
+
- ".gitignore"
|
63
|
+
- ".rspec"
|
64
|
+
- ".travis.yml"
|
65
|
+
- Gemfile
|
66
|
+
- LICENSE.txt
|
67
|
+
- README.md
|
68
|
+
- Rakefile
|
69
|
+
- bin/console
|
70
|
+
- bin/setup
|
71
|
+
- example.rb
|
72
|
+
- lib/logigram.rb
|
73
|
+
- lib/logigram/base.rb
|
74
|
+
- lib/logigram/challenge.rb
|
75
|
+
- lib/logigram/constraint.rb
|
76
|
+
- lib/logigram/formatter.rb
|
77
|
+
- lib/logigram/formatter/conjugations.rb
|
78
|
+
- lib/logigram/piece.rb
|
79
|
+
- lib/logigram/premise.rb
|
80
|
+
- lib/logigram/statistics.rb
|
81
|
+
- lib/logigram/version.rb
|
82
|
+
- logigram.gemspec
|
83
|
+
- test.rb
|
84
|
+
homepage: http://castwide.com
|
85
|
+
licenses:
|
86
|
+
- MIT
|
87
|
+
metadata: {}
|
88
|
+
post_install_message:
|
89
|
+
rdoc_options: []
|
90
|
+
require_paths:
|
91
|
+
- lib
|
92
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
98
|
+
requirements:
|
99
|
+
- - ">"
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: 1.3.1
|
102
|
+
requirements: []
|
103
|
+
rubygems_version: 3.3.7
|
104
|
+
signing_key:
|
105
|
+
specification_version: 4
|
106
|
+
summary: A library for generating logic puzzles
|
107
|
+
test_files: []
|