sliding_puzzle 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|