sliding_puzzle 1.0.0
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/README.md +269 -0
- data/lib/sliding_puzzle/base.rb +136 -0
- data/lib/sliding_puzzle/oracle.rb +109 -0
- data/lib/sliding_puzzle.rb +6 -0
- metadata +75 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: b1d7484d10c76bbf7e0f8be38bb08988ea6a5973e4d523c8cbbd114a5516c1dc
|
4
|
+
data.tar.gz: 850ae2da5a00df634aa1e7ef850ad2171a33a912925ad61c9a0999b697577269
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 8185f6328ed9afca9cdff45dd7fad6e9e6c8e270e7a0cea049eac5a3ff4ce0023e8e9604e93f6a35c4084fcfe51091db51a267daccbd64e9059c24319b69c6cd
|
7
|
+
data.tar.gz: 94215e1af28e0f89f6acf436e2c71d214093fa42221078b2a7dad5ec3914eb6e6cf16a4f29e0f283b7603a1e584e7b88658ef5cf3274c6437973cf6454dec7c6
|
data/README.md
ADDED
@@ -0,0 +1,269 @@
|
|
1
|
+
## Sliding Puzzle ##
|
2
|
+
|
3
|
+
A Ruby gem for manipulating and solving sliding tile puzzles.
|
4
|
+
|
5
|
+
**TODO - this gem is a work in progress**
|
6
|
+
|
7
|
+
### Overview ###
|
8
|
+
|
9
|
+
You might have come across sliding tile puzzles before. They're usually cheap,
|
10
|
+
plastic-y toys that can be rearranged by sliding their tiles around. Here's an
|
11
|
+
example containing a picture of a frog:
|
12
|
+
|
13
|
+
<br/><p align="center">
|
14
|
+
<img src="https://raw.githubusercontent.com/tuzz/sliding_puzzle/master/frog.jpg" />
|
15
|
+
</p><br/>
|
16
|
+
|
17
|
+
One of the pieces is blank which means an adjacent tile can move into its place.
|
18
|
+
After repeating this a few times, these puzzles can be tricky to solve. We can
|
19
|
+
also think of these puzzles as grids of numbers:
|
20
|
+
|
21
|
+
<br/><p align="center">
|
22
|
+
<img src="https://raw.githubusercontent.com/tuzz/sliding_puzzle/master/grid.jpg" />
|
23
|
+
</p><br/>
|
24
|
+
|
25
|
+
The challenge is to find a sequence of moves to rearrange the 'start state' into
|
26
|
+
the 'goal state' in as few moves as possible. This gem lets you play with these
|
27
|
+
puzzles and it solves them in an optimal number of moves.
|
28
|
+
|
29
|
+
In this example the blank is in the the upper-left corner of the goal state, but
|
30
|
+
it's arbitrary where it's located.
|
31
|
+
|
32
|
+
### Usage ###
|
33
|
+
|
34
|
+
Puzzles are represented as arrays of numbers:
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
puzzle = SlidingPuzzle.new(
|
38
|
+
[1, 2, 0], # <-- the 0 represents blank
|
39
|
+
[3, 4, 5],
|
40
|
+
[6, 7, 8],
|
41
|
+
)
|
42
|
+
```
|
43
|
+
|
44
|
+
You can slide tiles around and print the result:
|
45
|
+
|
46
|
+
```ruby
|
47
|
+
puzzle.slide!(:right)
|
48
|
+
puzzle.print
|
49
|
+
# [1, 0, 2]
|
50
|
+
# [3, 4, 5]
|
51
|
+
# [6, 7, 8]
|
52
|
+
```
|
53
|
+
|
54
|
+
The `#slide` method will return a new `SlidingPuzzle` whereas `#slide!` will
|
55
|
+
mutate the existing one.
|
56
|
+
|
57
|
+
### Moves ###
|
58
|
+
|
59
|
+
You can return an array of possible moves for a sliding puzzle:
|
60
|
+
|
61
|
+
```ruby
|
62
|
+
puzzle = SlidingPuzzle.new(
|
63
|
+
[1, 2, 0],
|
64
|
+
[3, 4, 5],
|
65
|
+
[6, 7, 8],
|
66
|
+
)
|
67
|
+
|
68
|
+
puzzle.moves
|
69
|
+
#=> [:right, :up]
|
70
|
+
```
|
71
|
+
|
72
|
+
If you try a move that isn't valid, an error is raised:
|
73
|
+
|
74
|
+
```ruby
|
75
|
+
puzzle.slide(:left)
|
76
|
+
# SlidingPuzzle::InvalidMoveError, "unable to slide left"
|
77
|
+
```
|
78
|
+
|
79
|
+
### Scrambling ###
|
80
|
+
|
81
|
+
You can scramble a puzzle:
|
82
|
+
|
83
|
+
```ruby
|
84
|
+
puzzle.scramble!
|
85
|
+
```
|
86
|
+
|
87
|
+
By default, this will perform 100 random moves, but you can set this:
|
88
|
+
|
89
|
+
```ruby
|
90
|
+
puzzle.scramble!(moves: 3)
|
91
|
+
```
|
92
|
+
|
93
|
+
The `#scramble` method will return a new `SlidingPuzzle` whereas `#scramble!`
|
94
|
+
will mutate the existing one.
|
95
|
+
|
96
|
+
### Dimensions ###
|
97
|
+
|
98
|
+
Puzzles can have different dimensions:
|
99
|
+
|
100
|
+
```ruby
|
101
|
+
two_by_four = SlidingPuzzle.new(
|
102
|
+
[1, 2, 3, 4],
|
103
|
+
[5, 6, 7, 0],
|
104
|
+
)
|
105
|
+
|
106
|
+
two_by_four.slide!(:down)
|
107
|
+
two_by_four.print
|
108
|
+
# [1, 2, 3, 0]
|
109
|
+
# [5, 6, 7, 4]
|
110
|
+
```
|
111
|
+
|
112
|
+
Puzzles must be rectangular and contain a single blank.
|
113
|
+
|
114
|
+
### Solving ###
|
115
|
+
|
116
|
+
Finding the shortest solution for a sliding puzzle is
|
117
|
+
[a hard problem](https://en.wikipedia.org/wiki/15_puzzle#Solvability). This gem
|
118
|
+
provides 'oracles' to find these solutions:
|
119
|
+
|
120
|
+
```ruby
|
121
|
+
goal_state = SlidingPuzzle.new(
|
122
|
+
[1, 2, 0],
|
123
|
+
[3, 4, 5],
|
124
|
+
[6, 7, 8],
|
125
|
+
)
|
126
|
+
|
127
|
+
oracle = SlidingPuzzle.oracle(goal_state)
|
128
|
+
```
|
129
|
+
|
130
|
+
This 'oracle' finds the shortest solution from any start state:
|
131
|
+
|
132
|
+
```ruby
|
133
|
+
start_state = SlidingPuzzle.new(
|
134
|
+
[1, 4, 2],
|
135
|
+
[3, 7, 5],
|
136
|
+
[6, 0, 8],
|
137
|
+
)
|
138
|
+
|
139
|
+
oracle.solve(start_state)
|
140
|
+
#=> [:down, :down, :left]
|
141
|
+
```
|
142
|
+
|
143
|
+
### Oracles ###
|
144
|
+
|
145
|
+
Oracles aren't magic. They are the result of precomputing solutions in advance.
|
146
|
+
This gem provides oracles for puzzles with up to eight tiles:
|
147
|
+
|
148
|
+
```ruby
|
149
|
+
goal_state = SlidingPuzzle.new(
|
150
|
+
[1, 2, 3, 4],
|
151
|
+
[5, 0, 6, 7],
|
152
|
+
)
|
153
|
+
|
154
|
+
oracle = SlidingPuzzle.oracle(goal_state)
|
155
|
+
```
|
156
|
+
|
157
|
+
The numbers of the goal state must be sequential, but the blank can be anywhere.
|
158
|
+
|
159
|
+
The `#oracle` method will return `nil` for a puzzle with more than eight tiles,
|
160
|
+
or if the numbers aren't sequential.
|
161
|
+
|
162
|
+
### Impossible puzzles ###
|
163
|
+
|
164
|
+
Some starting positions are
|
165
|
+
[impossible to solve](https://en.wikipedia.org/wiki/15_puzzle#Solvability). For
|
166
|
+
example, if you swap any two tiles from the goal state, there's no way to solve
|
167
|
+
the puzzle:
|
168
|
+
|
169
|
+
```ruby
|
170
|
+
unsolvable = SlidingPuzzle.new(
|
171
|
+
[2, 1, 0],
|
172
|
+
[3, 4, 5],
|
173
|
+
[6, 7, 8],
|
174
|
+
)
|
175
|
+
|
176
|
+
oracle.solve(unsolvable)
|
177
|
+
#=> nil
|
178
|
+
```
|
179
|
+
|
180
|
+
In total, there are [N!](https://en.wikipedia.org/wiki/Factorial) possible
|
181
|
+
configurations for a puzzle with N tiles (including the blank), but only half of
|
182
|
+
these are solvable.
|
183
|
+
|
184
|
+
For the 3x3 puzzle, there are 9! / 2 = 181,400 solvable configurations.
|
185
|
+
|
186
|
+
### Precomputing ###
|
187
|
+
|
188
|
+
For dimensions with no oracles, you can precompute your own:
|
189
|
+
|
190
|
+
```ruby
|
191
|
+
goal_state = SlidingPuzzle.new(
|
192
|
+
[0, 1, 2, 3],
|
193
|
+
[4, 5, 6, 7],
|
194
|
+
[8, 9, 10, 11],
|
195
|
+
)
|
196
|
+
|
197
|
+
oracle = SlidingPuzzle.precompute(goal_state)
|
198
|
+
```
|
199
|
+
|
200
|
+
You can then write the result to a file:
|
201
|
+
|
202
|
+
```ruby
|
203
|
+
oracle.write("path/to/file")
|
204
|
+
```
|
205
|
+
|
206
|
+
And read it in later:
|
207
|
+
|
208
|
+
```ruby
|
209
|
+
oracle = SlidingPuzzle.read("path/to/file")
|
210
|
+
|
211
|
+
start_state = SlidingPuzzle.new(
|
212
|
+
[1, 5, 2, 3],
|
213
|
+
[4, 0, 6, 7],
|
214
|
+
[8, 9, 10, 11],
|
215
|
+
)
|
216
|
+
|
217
|
+
oracle.solve(start_state)
|
218
|
+
#=> [:down, :right]
|
219
|
+
```
|
220
|
+
|
221
|
+
For puzzles with greater than 12 tiles, you won't be able to precompute an
|
222
|
+
oracle in a reasonable amount of time. The 4x4 puzzle will take more than 40,000
|
223
|
+
times longer to precompute than the 3x4 puzzle and require terrabytes of RAM.
|
224
|
+
The `debug` flag will reveal if it's ever likely to finish:
|
225
|
+
|
226
|
+
```ruby
|
227
|
+
SlidingPuzzle.precompute(goal_state, debug: true)
|
228
|
+
# queue size: 1
|
229
|
+
# queue size: 2
|
230
|
+
# queue size: 3
|
231
|
+
# ...
|
232
|
+
```
|
233
|
+
|
234
|
+
If it just keeps growing, it's unlikely to finish.
|
235
|
+
|
236
|
+
### Other methods ###
|
237
|
+
|
238
|
+
There are a few other methods that may be useful:
|
239
|
+
|
240
|
+
```ruby
|
241
|
+
# Get the number on the first row and second column:
|
242
|
+
puzzle.get(0, 1)
|
243
|
+
#=> 5
|
244
|
+
|
245
|
+
# Find the row and column of the number 5:
|
246
|
+
puzzle.find(5)
|
247
|
+
#=> [0, 1]
|
248
|
+
|
249
|
+
# Return a clone of the array of tiles:
|
250
|
+
puzzle.tiles
|
251
|
+
#=> [[1, 5, 2, 3], [4, 0, 6, 7], [8, 9, 10, 11]]
|
252
|
+
|
253
|
+
# Return a clone of the puzzle:
|
254
|
+
puzzle.clone
|
255
|
+
#=> #<SlidingPuzzle:object_id>
|
256
|
+
```
|
257
|
+
|
258
|
+
### Ideas to try ###
|
259
|
+
|
260
|
+
I hope you have fun with this gem. Here are some things to try:
|
261
|
+
|
262
|
+
1) Find an algorithm to solve a puzzle without using an oracle
|
263
|
+
2) Compare the number of moves your algorithm makes with the oracle
|
264
|
+
3) Write a web server for interacting with sliding puzzles
|
265
|
+
4) Read
|
266
|
+
[more](https://www.ijcai.org/Proceedings/03/Papers/267.pdf)
|
267
|
+
[about](https://www.ijcai.org/Proceedings/03/Papers/267.pdf)
|
268
|
+
techniques for coping with larger dimensions
|
269
|
+
5) Write a user interface to slide tiles around
|
@@ -0,0 +1,136 @@
|
|
1
|
+
class SlidingPuzzle
|
2
|
+
def self.oracle(goal_state)
|
3
|
+
Oracle.lookup(goal_state)
|
4
|
+
end
|
5
|
+
|
6
|
+
def self.precompute(goal_state, **options)
|
7
|
+
Oracle.precompute(goal_state, **options)
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.read(path)
|
11
|
+
Oracle.read(path)
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(*tiles)
|
15
|
+
self.tiles = flatten_tiles(tiles)
|
16
|
+
self.max_row = @tiles.size - 1
|
17
|
+
self.max_column = @tiles.first.size - 1
|
18
|
+
|
19
|
+
must_be_rectangular!
|
20
|
+
must_contain_one_blank!
|
21
|
+
end
|
22
|
+
|
23
|
+
def slide!(direction)
|
24
|
+
unless moves.include?(direction)
|
25
|
+
raise InvalidMoveError, "unable to slide #{direction}"
|
26
|
+
end
|
27
|
+
|
28
|
+
x1, y1 = find(0)
|
29
|
+
x2, y2 = x1, y1
|
30
|
+
|
31
|
+
y2 += 1 if direction == :left
|
32
|
+
y2 -= 1 if direction == :right
|
33
|
+
x2 += 1 if direction == :up
|
34
|
+
x2 -= 1 if direction == :down
|
35
|
+
|
36
|
+
@tiles[x1][y1], @tiles[x2][y2] = @tiles[x2][y2], @tiles[x1][y1]
|
37
|
+
self
|
38
|
+
end
|
39
|
+
|
40
|
+
def slide(direction)
|
41
|
+
clone.slide!(direction)
|
42
|
+
end
|
43
|
+
|
44
|
+
def clone
|
45
|
+
self.class.new(*tiles)
|
46
|
+
end
|
47
|
+
|
48
|
+
def print
|
49
|
+
Kernel.print @tiles.map(&:inspect).join("\n")
|
50
|
+
end
|
51
|
+
|
52
|
+
def moves
|
53
|
+
row, column = find(0)
|
54
|
+
moves = []
|
55
|
+
|
56
|
+
moves.push(:left) unless column == max_column
|
57
|
+
moves.push(:right) unless column.zero?
|
58
|
+
moves.push(:up) unless row == max_row
|
59
|
+
moves.push(:down) unless row.zero?
|
60
|
+
|
61
|
+
moves
|
62
|
+
end
|
63
|
+
|
64
|
+
def scramble!(moves: 100)
|
65
|
+
moves.times do
|
66
|
+
slide!(self.moves.sample)
|
67
|
+
end
|
68
|
+
|
69
|
+
self
|
70
|
+
end
|
71
|
+
|
72
|
+
def scramble(moves: 100)
|
73
|
+
clone.scramble!(moves: moves)
|
74
|
+
end
|
75
|
+
|
76
|
+
def tiles
|
77
|
+
JSON.parse(JSON.generate(@tiles))
|
78
|
+
end
|
79
|
+
|
80
|
+
def get(row, column)
|
81
|
+
@tiles[row][column]
|
82
|
+
end
|
83
|
+
|
84
|
+
def find(number)
|
85
|
+
tiles.each.with_index do |numbers, row|
|
86
|
+
numbers.each.with_index do |n, column|
|
87
|
+
return [row, column] if n == number
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
nil
|
92
|
+
end
|
93
|
+
|
94
|
+
def ==(other)
|
95
|
+
tiles == other.tiles
|
96
|
+
end
|
97
|
+
|
98
|
+
alias :eql? :==
|
99
|
+
|
100
|
+
def hash
|
101
|
+
tiles.hash
|
102
|
+
end
|
103
|
+
|
104
|
+
private
|
105
|
+
|
106
|
+
def flatten_tiles(tiles)
|
107
|
+
if tiles[0].is_a?(Array) && tiles[0][0].is_a?(Array)
|
108
|
+
tiles.flatten(1)
|
109
|
+
else
|
110
|
+
tiles
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def must_be_rectangular!
|
115
|
+
sizes = tiles.map(&:size)
|
116
|
+
|
117
|
+
if sizes.uniq.size > 1
|
118
|
+
raise NotRectangularError, "puzzle must be rectangular"
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def must_contain_one_blank!
|
123
|
+
blanks = tiles.flatten.count(0)
|
124
|
+
|
125
|
+
unless blanks == 1
|
126
|
+
raise BlankError, "puzzle must contain a single blank"
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
attr_accessor :max_row, :max_column
|
131
|
+
attr_writer :tiles
|
132
|
+
|
133
|
+
class InvalidMoveError < StandardError; end
|
134
|
+
class NotRectangularError < StandardError; end
|
135
|
+
class BlankError < StandardError; end
|
136
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
class SlidingPuzzle
|
2
|
+
class Oracle
|
3
|
+
OPPOSITES = {
|
4
|
+
left: :right,
|
5
|
+
right: :left,
|
6
|
+
up: :down,
|
7
|
+
down: :up,
|
8
|
+
}
|
9
|
+
|
10
|
+
def self.precompute(goal_state, debug: false)
|
11
|
+
goal_state = goal_state.clone
|
12
|
+
|
13
|
+
queue = [goal_state]
|
14
|
+
lookup_table = { goal_state => :goal }
|
15
|
+
|
16
|
+
until queue.empty?
|
17
|
+
puts "queue size: #{queue.size}" if debug
|
18
|
+
puzzle = queue.shift
|
19
|
+
|
20
|
+
puzzle.moves.each do |direction|
|
21
|
+
next_puzzle = puzzle.slide(direction)
|
22
|
+
|
23
|
+
unless lookup_table[next_puzzle]
|
24
|
+
lookup_table[next_puzzle] = OPPOSITES[direction]
|
25
|
+
queue.push(next_puzzle)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
new(lookup_table)
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.precompute_all(max_tiles: 8, directory: "oracles", debug: false)
|
34
|
+
FileUtils.mkdir_p("oracles")
|
35
|
+
|
36
|
+
1.upto(5) do |rows|
|
37
|
+
1.upto(5) do |columns|
|
38
|
+
number_of_tiles = rows * columns - 1
|
39
|
+
next if number_of_tiles > max_tiles
|
40
|
+
|
41
|
+
numbers = 1.upto(number_of_tiles).to_a
|
42
|
+
|
43
|
+
0.upto(number_of_tiles) do |position|
|
44
|
+
numbers_with_blank = numbers.dup.insert(position, 0)
|
45
|
+
tiles = numbers_with_blank.each_slice(columns).to_a
|
46
|
+
|
47
|
+
goal_state = SlidingPuzzle.new(tiles)
|
48
|
+
path = "#{directory}/#{basename(goal_state)}.dat"
|
49
|
+
|
50
|
+
print "Precomputing #{path}... " if debug
|
51
|
+
|
52
|
+
oracle = precompute(goal_state)
|
53
|
+
oracle.write(path)
|
54
|
+
|
55
|
+
puts "Done." if debug
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.lookup(goal_state)
|
62
|
+
filename = "#{basename(goal_state)}.dat"
|
63
|
+
path = File.expand_path("#{__dir__}/../../oracles/#{filename}")
|
64
|
+
|
65
|
+
read(path) if File.exist?(path)
|
66
|
+
end
|
67
|
+
|
68
|
+
def self.basename(puzzle)
|
69
|
+
puzzle.tiles.map { |row| row.join(",") }.join(":")
|
70
|
+
end
|
71
|
+
|
72
|
+
def initialize(lookup_table)
|
73
|
+
self.lookup_table = lookup_table
|
74
|
+
end
|
75
|
+
|
76
|
+
def solve(sliding_puzzle)
|
77
|
+
moves = []
|
78
|
+
next_puzzle = sliding_puzzle
|
79
|
+
|
80
|
+
loop do
|
81
|
+
direction = lookup_table[next_puzzle]
|
82
|
+
|
83
|
+
return nil unless direction
|
84
|
+
return moves if direction == :goal
|
85
|
+
|
86
|
+
moves.push(direction)
|
87
|
+
next_puzzle = next_puzzle.slide(direction)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def write(path)
|
92
|
+
data = Marshal.dump(self)
|
93
|
+
gzip = Zlib::Deflate.deflate(data)
|
94
|
+
|
95
|
+
File.open(path, "wb") { |f| f.write(gzip) }
|
96
|
+
end
|
97
|
+
|
98
|
+
def self.read(path)
|
99
|
+
gzip = File.binread(path)
|
100
|
+
data = Zlib::Inflate.inflate(gzip)
|
101
|
+
|
102
|
+
Marshal.load(data)
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
attr_accessor :lookup_table
|
108
|
+
end
|
109
|
+
end
|
metadata
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: sliding_puzzle
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Chris Patuzzo
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-01-29 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rspec
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 3.7.0
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 3.7.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: pry
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.11.3
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 0.11.3
|
41
|
+
description: A Ruby gem for manipulating and solving sliding tile puzzles.
|
42
|
+
email: chris@patuzzo.co.uk
|
43
|
+
executables: []
|
44
|
+
extensions: []
|
45
|
+
extra_rdoc_files: []
|
46
|
+
files:
|
47
|
+
- README.md
|
48
|
+
- lib/sliding_puzzle.rb
|
49
|
+
- lib/sliding_puzzle/base.rb
|
50
|
+
- lib/sliding_puzzle/oracle.rb
|
51
|
+
homepage: https://github.com/tuzz/sliding_puzzle
|
52
|
+
licenses:
|
53
|
+
- MIT
|
54
|
+
metadata: {}
|
55
|
+
post_install_message:
|
56
|
+
rdoc_options: []
|
57
|
+
require_paths:
|
58
|
+
- lib
|
59
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
60
|
+
requirements:
|
61
|
+
- - ">="
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
version: '0'
|
64
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
requirements: []
|
70
|
+
rubyforge_project:
|
71
|
+
rubygems_version: 2.7.3
|
72
|
+
signing_key:
|
73
|
+
specification_version: 4
|
74
|
+
summary: Sliding Puzzle
|
75
|
+
test_files: []
|