tuomas-knights_tour 0.2.5
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.rdoc +24 -0
- data/Manifest.txt +7 -0
- data/README.rdoc +82 -0
- data/Rakefile +48 -0
- data/bin/knights_tour +41 -0
- data/lib/knights_tour.rb +192 -0
- data/spec/knights_tour_spec.rb +172 -0
- metadata +92 -0
data/CHANGELOG.rdoc
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
=== 0.2.5 released 2009-01-14
|
2
|
+
|
3
|
+
* Small code beautifications.
|
4
|
+
|
5
|
+
=== 0.2.4 released 2008-12-10
|
6
|
+
|
7
|
+
* Small code optimization.
|
8
|
+
|
9
|
+
=== 0.2.3 released 2008-12-11
|
10
|
+
|
11
|
+
* Small documentation improvements.
|
12
|
+
* Refactored code.
|
13
|
+
|
14
|
+
=== 0.2.2 released 2008-12-10
|
15
|
+
|
16
|
+
* Small socumentation improvements.
|
17
|
+
|
18
|
+
=== 0.2.1 released 2008-12-10
|
19
|
+
|
20
|
+
* Refactored code.
|
21
|
+
|
22
|
+
=== 0.2.0 released 2008-12-10
|
23
|
+
|
24
|
+
* First revision that solves the problem correctly. :)
|
data/Manifest.txt
ADDED
data/README.rdoc
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
= Knight's Tour
|
2
|
+
|
3
|
+
A program that attempts to find a solution to the
|
4
|
+
{Knight's Tour problem}[http://en.wikipedia.org/wiki/Knight%27s_Tour].
|
5
|
+
I was inspired to do this by finding
|
6
|
+
{a Python implementation}[http://ttsiodras.googlepages.com/knightstour.html].
|
7
|
+
|
8
|
+
The program's algorithm is a recursive backtracking search that returns the
|
9
|
+
first solution to the problem (if the algorithm finds one). It utilizes
|
10
|
+
{Warnsdorff's heuristics}[http://mathworld.wolfram.com/KnightsTour.html] to
|
11
|
+
avoid dead ends, making the search faster in general.
|
12
|
+
|
13
|
+
== Installation
|
14
|
+
|
15
|
+
Install the software as a RubyGem from GitHub:
|
16
|
+
|
17
|
+
$ sudo gem install tuomas-knights_tour --source http://gems.github.com
|
18
|
+
|
19
|
+
The software is compatible with Ruby 1.9.1.
|
20
|
+
|
21
|
+
== Usage
|
22
|
+
|
23
|
+
Change the working directory of your command line shell into the project's
|
24
|
+
root directory and enter
|
25
|
+
|
26
|
+
$ ./bin/knights_tour
|
27
|
+
|
28
|
+
The command attempts to solve the problem on a board of size 8x8, the knight
|
29
|
+
located initially at position 0,0 (the top-left corner). If the program
|
30
|
+
finds a solution, it displays a result similar to the following:
|
31
|
+
|
32
|
+
+---+---+---+---+---+---+---+---+
|
33
|
+
| 1| 62| 13| 36| 3| 38| 31| 28|
|
34
|
+
+---+---+---+---+---+---+---+---+
|
35
|
+
| 14| 35| 2| 63| 32| 29| 4| 39|
|
36
|
+
+---+---+---+---+---+---+---+---+
|
37
|
+
| 61| 12| 59| 34| 37| 42| 27| 30|
|
38
|
+
+---+---+---+---+---+---+---+---+
|
39
|
+
| 50| 15| 64| 43| 58| 33| 40| 5|
|
40
|
+
+---+---+---+---+---+---+---+---+
|
41
|
+
| 11| 60| 49| 54| 41| 24| 45| 26|
|
42
|
+
+---+---+---+---+---+---+---+---+
|
43
|
+
| 16| 51| 18| 57| 44| 55| 6| 23|
|
44
|
+
+---+---+---+---+---+---+---+---+
|
45
|
+
| 19| 10| 53| 48| 21| 8| 25| 46|
|
46
|
+
+---+---+---+---+---+---+---+---+
|
47
|
+
| 52| 17| 20| 9| 56| 47| 22| 7|
|
48
|
+
+---+---+---+---+---+---+---+---+
|
49
|
+
|
50
|
+
The size of the board and the start position of the knight are configurable.
|
51
|
+
For all the options, see
|
52
|
+
|
53
|
+
$ ./bin/knights_tour -h
|
54
|
+
|
55
|
+
== Contacting
|
56
|
+
|
57
|
+
Please send feedback by email to Tuomas Kareinen < tkareine (at) gmail (dot)
|
58
|
+
com >.
|
59
|
+
|
60
|
+
== Legal notes
|
61
|
+
|
62
|
+
This software is licensed under the terms of the "MIT license":
|
63
|
+
|
64
|
+
Copyright (c) 2008-2009 Tuomas Kareinen.
|
65
|
+
|
66
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
67
|
+
of this software and associated documentation files (the "Software"), to
|
68
|
+
deal in the Software without restriction, including without limitation the
|
69
|
+
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
70
|
+
sell copies of the Software, and to permit persons to whom the Software is
|
71
|
+
furnished to do so, subject to the following conditions:
|
72
|
+
|
73
|
+
The above copyright notice and this permission notice shall be included in
|
74
|
+
all copies or substantial portions of the Software.
|
75
|
+
|
76
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
77
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
78
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
79
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
80
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
81
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
82
|
+
IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
require "rubygems"
|
2
|
+
|
3
|
+
require "hoe"
|
4
|
+
require "./lib/knights_tour"
|
5
|
+
Hoe.new("knights_tour", KnightsTour::Meta::VERSION.to_s) do |p|
|
6
|
+
p.author = "Tuomas Kareinen"
|
7
|
+
p.email = "tkareine@gmail.com"
|
8
|
+
p.url = "http://github.com/tuomas/knights_tour"
|
9
|
+
p.summary =<<-END
|
10
|
+
A program that attempts to find a solution to the Knight's Tour problem.
|
11
|
+
END
|
12
|
+
p.readme_file = "README.rdoc"
|
13
|
+
p.history_file = "CHANGELOG.rdoc"
|
14
|
+
p.extra_rdoc_files = FileList["*.rdoc", "lib/**/*.rb"]
|
15
|
+
p.extra_deps = [["trollop", ">= 1.10.0"]]
|
16
|
+
p.extra_dev_deps = [["rspec", ">= 1.2.0"]]
|
17
|
+
p.rubyforge_name = "searchable-rec"
|
18
|
+
end
|
19
|
+
|
20
|
+
require "rake/rdoctask"
|
21
|
+
require "lib/knights_tour"
|
22
|
+
desc "Create documentation."
|
23
|
+
Rake::RDocTask.new(:rdoc) do |rd|
|
24
|
+
rd.title = "Knight's Tour #{KnightsTour::Meta::VERSION}"
|
25
|
+
rd.main = "README.rdoc"
|
26
|
+
rd.rdoc_files.include("*.rdoc", "lib/**/*.rb")
|
27
|
+
rd.rdoc_dir = "rdoc"
|
28
|
+
end
|
29
|
+
|
30
|
+
require "spec/rake/spectask"
|
31
|
+
desc "Run specs."
|
32
|
+
Spec::Rake::SpecTask.new(:spec) do |t|
|
33
|
+
t.spec_files = FileList["spec/**/*.rb"]
|
34
|
+
t.spec_opts = ["--format", "specdoc"]
|
35
|
+
#t.warning = true
|
36
|
+
end
|
37
|
+
|
38
|
+
desc "Find code smells."
|
39
|
+
task :roodi do
|
40
|
+
sh("roodi '**/*.rb'")
|
41
|
+
end
|
42
|
+
|
43
|
+
desc "Search unfinished parts of source code."
|
44
|
+
task :todo do
|
45
|
+
FileList["**/*.rb"].egrep /#.*(TODO|FIXME)/
|
46
|
+
end
|
47
|
+
|
48
|
+
task :default => :spec
|
data/bin/knights_tour
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "rubygems"
|
4
|
+
require "trollop"
|
5
|
+
require File.dirname(__FILE__) << "/../lib/knights_tour"
|
6
|
+
|
7
|
+
# Parse the command line arguments and invoke the application.
|
8
|
+
|
9
|
+
include KnightsTour
|
10
|
+
|
11
|
+
options = Trollop::options do
|
12
|
+
version Meta.version
|
13
|
+
|
14
|
+
banner <<-EOS
|
15
|
+
A program that attempts to find a solution to the Knight's Tour problem.
|
16
|
+
|
17
|
+
Usage:
|
18
|
+
|
19
|
+
#{File.basename($0)} [OPTIONS] [SIZE]
|
20
|
+
|
21
|
+
Where SIZE (default: 8,8) sets the size of the board to ROWS x COLS.
|
22
|
+
|
23
|
+
Options:
|
24
|
+
EOS
|
25
|
+
|
26
|
+
opt :start_at, "Set the initial position of the knight (default: 0,0)",
|
27
|
+
:type => :string,
|
28
|
+
:required => false
|
29
|
+
end
|
30
|
+
|
31
|
+
begin
|
32
|
+
app_params = {}
|
33
|
+
app_params[:size] = ARGV.shift unless ARGV.empty?
|
34
|
+
app_params[:start_at] = options[:start_at] if options[:start_at]
|
35
|
+
|
36
|
+
app = Application.new(app_params)
|
37
|
+
rescue ArgumentError => e
|
38
|
+
Trollop::die e.to_s
|
39
|
+
end
|
40
|
+
|
41
|
+
$stdout.puts app.solve
|
data/lib/knights_tour.rb
ADDED
@@ -0,0 +1,192 @@
|
|
1
|
+
module KnightsTour
|
2
|
+
module Meta #:nodoc:
|
3
|
+
module VERSION #:nodoc:
|
4
|
+
MAJOR = 0
|
5
|
+
MINOR = 2
|
6
|
+
PATCH = 5
|
7
|
+
|
8
|
+
def self.to_s
|
9
|
+
[ MAJOR, MINOR, PATCH ].join(".")
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
COPYRIGHT = "Copyright (c) Tuomas Kareinen"
|
14
|
+
|
15
|
+
LICENSE = "Licensed under the terms of the \"MIT license\". See README.rdoc."
|
16
|
+
|
17
|
+
def self.version
|
18
|
+
"#{File.basename($0)} #{Meta::VERSION}\n#{Meta::COPYRIGHT}\n#{Meta::LICENSE}"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class Application
|
23
|
+
def initialize(params = {})
|
24
|
+
@board_size = parse_board_size(params[:size] || [8, 8])
|
25
|
+
@knight_starts_at = parse_position_on_board(
|
26
|
+
params[:start_at] || [0, 0],
|
27
|
+
@board_size)
|
28
|
+
end
|
29
|
+
|
30
|
+
def solve
|
31
|
+
@solution ||= StringResult.new(traverse(
|
32
|
+
Knight.new(@board_size, @knight_starts_at)))
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def parse_pair(param)
|
38
|
+
unless param.is_a?(Array)
|
39
|
+
param = param.to_s.split(",")
|
40
|
+
end
|
41
|
+
[param[0].to_i, param[1].to_i]
|
42
|
+
end
|
43
|
+
|
44
|
+
def parse_board_size(size)
|
45
|
+
size = parse_pair(size)
|
46
|
+
unless size[0] > 0 && size[1] > 0
|
47
|
+
raise ArgumentError,
|
48
|
+
"Board size must be a pair of positive (non-zero) " \
|
49
|
+
"integers, separated by a comma"
|
50
|
+
end
|
51
|
+
size
|
52
|
+
end
|
53
|
+
|
54
|
+
def parse_position_on_board(position, board_size)
|
55
|
+
position = parse_pair(position)
|
56
|
+
unless (0...board_size[0]).include?(position[0]) &&
|
57
|
+
(0...board_size[1]).include?(position[1])
|
58
|
+
raise ArgumentError,
|
59
|
+
"Position must be a pair of positive integers within the " \
|
60
|
+
"size limits of the board, separated by a comma " \
|
61
|
+
"(for example, 0,5 is acceptable for board size 6,6)"
|
62
|
+
end
|
63
|
+
position
|
64
|
+
end
|
65
|
+
|
66
|
+
# Traverse the knight on the board.
|
67
|
+
#
|
68
|
+
# The algorithm is a recursive backtracking search for a first solution
|
69
|
+
# to the problem. The board is copied and modified by moving the knight
|
70
|
+
# to a new position in each recursive step of the algorithm, instead of
|
71
|
+
# modifying a single shared board in place.
|
72
|
+
def traverse(knight)
|
73
|
+
#$stdout.puts StringResult.new(board) # debug
|
74
|
+
|
75
|
+
unless knight.traversed?
|
76
|
+
next_positions = knight.find_next_positions
|
77
|
+
next_positions.each do |next_position|
|
78
|
+
knight = traverse(knight.dup.traverse_to(next_position))
|
79
|
+
unless knight.nil?
|
80
|
+
return knight # return the first solution found
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
knight # no solutions found, or already found one
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
class Knight
|
90
|
+
## as [x, y] pairs
|
91
|
+
LEGAL_STEPS = [ [-2, 1], [-1, 2], [ 1, 2], [ 2, 1],
|
92
|
+
[ 2, -1], [ 1, -2], [-1, -2], [-2, -1] ]
|
93
|
+
|
94
|
+
attr_reader :board, :steps_taken, :current_position
|
95
|
+
|
96
|
+
def initialize(board_size, start_at)
|
97
|
+
@board = Array.new(board_size[0]) { Array.new(board_size[1], 0) }
|
98
|
+
@steps_taken = 0
|
99
|
+
traverse_to(start_at)
|
100
|
+
end
|
101
|
+
|
102
|
+
def initialize_copy(other)
|
103
|
+
@board = Marshal.load(Marshal.dump(other.board))
|
104
|
+
@steps_taken = other.steps_taken
|
105
|
+
end
|
106
|
+
|
107
|
+
def traversed?
|
108
|
+
last_step = @board.size * @board[0].size
|
109
|
+
@steps_taken == last_step
|
110
|
+
end
|
111
|
+
|
112
|
+
def traverse_to(new_position)
|
113
|
+
@steps_taken += 1
|
114
|
+
@current_position = new_position
|
115
|
+
@board[@current_position[0]][@current_position[1]] = @steps_taken
|
116
|
+
self
|
117
|
+
end
|
118
|
+
|
119
|
+
def find_next_positions
|
120
|
+
sort_by_warnsdorffs_heuristics(find_next_positions_at(@current_position))
|
121
|
+
end
|
122
|
+
|
123
|
+
private
|
124
|
+
|
125
|
+
# Optimization by applying Warnsdorff's heuristics: attempt to avoid
|
126
|
+
# dead ends by favoring positions with the lowest number of next
|
127
|
+
# available positions (thus, isolated positions become visited first).
|
128
|
+
#
|
129
|
+
# References:
|
130
|
+
# <http://mathworld.wolfram.com/KnightsTour.html>
|
131
|
+
# <http://web.telia.com/~u85905224/knight/eWarnsd.htm>
|
132
|
+
def sort_by_warnsdorffs_heuristics(positions)
|
133
|
+
positions.sort_by do |position|
|
134
|
+
find_next_positions_at(position).size
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def find_next_positions_at(position)
|
139
|
+
positions = LEGAL_STEPS.map do |step|
|
140
|
+
position_after_step(position, step)
|
141
|
+
end
|
142
|
+
positions.reject { |pos| pos.nil? || (@board[pos[0]][pos[1]] > 0) }
|
143
|
+
end
|
144
|
+
|
145
|
+
def position_after_step(from, step)
|
146
|
+
x_pos = from[0] + step[0]
|
147
|
+
y_pos = from[1] + step[1]
|
148
|
+
|
149
|
+
if (0...@board.size).include?(x_pos) &&
|
150
|
+
(0...@board[0].size).include?(y_pos)
|
151
|
+
[x_pos, y_pos]
|
152
|
+
else
|
153
|
+
nil
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
class StringResult
|
159
|
+
def initialize(result)
|
160
|
+
if result.is_a?(Knight)
|
161
|
+
@result = board_to_s(result.board, result.steps_taken)
|
162
|
+
else
|
163
|
+
@result = "No solution found."
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def to_s
|
168
|
+
@result
|
169
|
+
end
|
170
|
+
|
171
|
+
private
|
172
|
+
|
173
|
+
def board_to_s(board, steps_taken)
|
174
|
+
square_width = steps_taken.to_s.length + 1
|
175
|
+
separator_str = separator(square_width, board[0].size)
|
176
|
+
|
177
|
+
output = ""
|
178
|
+
|
179
|
+
board.each do |row|
|
180
|
+
output << separator_str
|
181
|
+
row_output = row.map { |step| "%#{square_width}s" % step }.join("|")
|
182
|
+
output << "|#{row_output}|\n"
|
183
|
+
end
|
184
|
+
|
185
|
+
output << separator_str
|
186
|
+
end
|
187
|
+
|
188
|
+
def separator(board_width, cols)
|
189
|
+
("+" << "-" * board_width) * cols << "+\n"
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
@@ -0,0 +1,172 @@
|
|
1
|
+
require File.dirname(__FILE__) << "/../lib/knights_tour"
|
2
|
+
|
3
|
+
include KnightsTour
|
4
|
+
|
5
|
+
describe Application do
|
6
|
+
it "should accept valid non-default board size" do
|
7
|
+
lambda { Application.new(:size => -1) }.should raise_error(ArgumentError)
|
8
|
+
lambda { Application.new(:size => 0) }.should raise_error(ArgumentError)
|
9
|
+
lambda { Application.new(:size => 1) }.should raise_error(ArgumentError)
|
10
|
+
lambda { Application.new(:size => "1") }.should raise_error(ArgumentError)
|
11
|
+
lambda { Application.new(:size => [-1, -1]) }.should raise_error(ArgumentError)
|
12
|
+
lambda { Application.new(:size => [-1, 0]) }.should raise_error(ArgumentError)
|
13
|
+
lambda { Application.new(:size => [0, -1]) }.should raise_error(ArgumentError)
|
14
|
+
lambda { Application.new(:size => [0, 0]) }.should raise_error(ArgumentError)
|
15
|
+
lambda { Application.new(:size => [0, 1]) }.should raise_error(ArgumentError)
|
16
|
+
lambda { Application.new(:size => [1, 0]) }.should raise_error(ArgumentError)
|
17
|
+
lambda { Application.new(:size => [1, 1]) }.should_not raise_error(ArgumentError)
|
18
|
+
lambda { Application.new(:size => [3, 4]) }.should_not raise_error(ArgumentError)
|
19
|
+
lambda { Application.new(:size => [5, 5]) }.should_not raise_error(ArgumentError)
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should accept valid non-default start positions" do
|
23
|
+
lambda { Application.new(:size => [1, 1], :start_at => -1) }.should raise_error(ArgumentError)
|
24
|
+
lambda { Application.new(:size => [1, 1], :start_at => [ 0, 1]) }.should raise_error(ArgumentError)
|
25
|
+
lambda { Application.new(:size => [1, 1], :start_at => [ 1, 0]) }.should raise_error(ArgumentError)
|
26
|
+
lambda { Application.new(:size => [1, 1], :start_at => [ 1, 1]) }.should raise_error(ArgumentError)
|
27
|
+
lambda { Application.new(:size => [1, 1], :start_at => [ 3, 4]) }.should raise_error(ArgumentError)
|
28
|
+
lambda { Application.new(:size => [3, 7], :start_at => [ 4, 2]) }.should raise_error(ArgumentError)
|
29
|
+
lambda { Application.new(:size => [3, 7], :start_at => [ 2, 7]) }.should raise_error(ArgumentError)
|
30
|
+
lambda { Application.new(:size => [3, 7], :start_at => [ 3, 7]) }.should raise_error(ArgumentError)
|
31
|
+
lambda { Application.new(:size => [1, 1], :start_at => [ 0, 0]) }.should_not raise_error(ArgumentError)
|
32
|
+
lambda { Application.new(:size => [1, 2], :start_at => [ 0, 1]) }.should_not raise_error(ArgumentError)
|
33
|
+
lambda { Application.new(:size => [2, 1], :start_at => [ 1, 0]) }.should_not raise_error(ArgumentError)
|
34
|
+
lambda { Application.new(:size => [2, 2], :start_at => [ 0, 0]) }.should_not raise_error(ArgumentError)
|
35
|
+
lambda { Application.new(:size => [2, 2], :start_at => [ 1, 1]) }.should_not raise_error(ArgumentError)
|
36
|
+
lambda { Application.new(:size => [3, 7], :start_at => [ 2, 4]) }.should_not raise_error(ArgumentError)
|
37
|
+
lambda { Application.new(:size => [5, 5], :start_at => [ 4, 4]) }.should_not raise_error(ArgumentError)
|
38
|
+
lambda { Application.new(:size => [8, 8], :start_at => [ 7, 6]) }.should_not raise_error(ArgumentError)
|
39
|
+
end
|
40
|
+
|
41
|
+
it "should solve a board with size 1,1" do
|
42
|
+
result = Application.new(:size => [1, 1]).solve
|
43
|
+
result.to_s.should == <<-END
|
44
|
+
+--+
|
45
|
+
| 1|
|
46
|
+
+--+
|
47
|
+
END
|
48
|
+
end
|
49
|
+
|
50
|
+
it "should solve a board with size 5,5" do
|
51
|
+
result = Application.new(:size => [5, 5]).solve
|
52
|
+
result.to_s.should == <<-END
|
53
|
+
+---+---+---+---+---+
|
54
|
+
| 1| 14| 9| 20| 3|
|
55
|
+
+---+---+---+---+---+
|
56
|
+
| 24| 19| 2| 15| 10|
|
57
|
+
+---+---+---+---+---+
|
58
|
+
| 13| 8| 25| 4| 21|
|
59
|
+
+---+---+---+---+---+
|
60
|
+
| 18| 23| 6| 11| 16|
|
61
|
+
+---+---+---+---+---+
|
62
|
+
| 7| 12| 17| 22| 5|
|
63
|
+
+---+---+---+---+---+
|
64
|
+
END
|
65
|
+
end
|
66
|
+
|
67
|
+
it "should solve a board with size of 5,5, in start position 2,2" do
|
68
|
+
result = Application.new(:size => [5, 5], :start_at => [2, 2]).solve
|
69
|
+
result.to_s.should == <<-END
|
70
|
+
+---+---+---+---+---+
|
71
|
+
| 21| 12| 7| 2| 19|
|
72
|
+
+---+---+---+---+---+
|
73
|
+
| 6| 17| 20| 13| 8|
|
74
|
+
+---+---+---+---+---+
|
75
|
+
| 11| 22| 1| 18| 3|
|
76
|
+
+---+---+---+---+---+
|
77
|
+
| 16| 5| 24| 9| 14|
|
78
|
+
+---+---+---+---+---+
|
79
|
+
| 23| 10| 15| 4| 25|
|
80
|
+
+---+---+---+---+---+
|
81
|
+
END
|
82
|
+
end
|
83
|
+
|
84
|
+
it "should solve a board with size of 3,7, in start position 2,4" do
|
85
|
+
result = Application.new(:size => [3, 7], :start_at => [2, 4]).solve
|
86
|
+
result.to_s.should == <<-END
|
87
|
+
+---+---+---+---+---+---+---+
|
88
|
+
| 11| 14| 17| 20| 3| 8| 5|
|
89
|
+
+---+---+---+---+---+---+---+
|
90
|
+
| 16| 21| 12| 9| 6| 19| 2|
|
91
|
+
+---+---+---+---+---+---+---+
|
92
|
+
| 13| 10| 15| 18| 1| 4| 7|
|
93
|
+
+---+---+---+---+---+---+---+
|
94
|
+
END
|
95
|
+
end
|
96
|
+
|
97
|
+
it "should cache the result" do
|
98
|
+
app = Application.new(:size => [1, 1])
|
99
|
+
result = []
|
100
|
+
result << app.solve
|
101
|
+
result << app.solve
|
102
|
+
result[0].should == result[1]
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
describe Knight do
|
107
|
+
before(:each) do
|
108
|
+
@knight = Knight.new([5, 5], [0, 0])
|
109
|
+
# broken board state, but it does not matter for testing
|
110
|
+
@knight.instance_variable_set(
|
111
|
+
:@board,
|
112
|
+
[ [ 1, 0, 0, 12, 3 ],
|
113
|
+
[ 0, 11, 2, 7, 18 ],
|
114
|
+
[ 0, 0, 0, 0, 13 ],
|
115
|
+
[ 10, 15, 6, 0, 8 ],
|
116
|
+
[ 0, 19, 9, 14, 5 ] ])
|
117
|
+
@knight.instance_variable_set(:@current_position, [1, 4])
|
118
|
+
@knight.instance_variable_set(:@steps_taken, 18)
|
119
|
+
end
|
120
|
+
|
121
|
+
it "should find next positions available" do
|
122
|
+
next_positions = @knight.send(:find_next_positions_at, @knight.current_position)
|
123
|
+
next_positions.sort! # ensure the order is not correct already
|
124
|
+
next_positions.should == [ [0, 2], [2, 2], [3, 3] ]
|
125
|
+
end
|
126
|
+
|
127
|
+
it "should sort next positions by Warnsdorff's heuristics" do
|
128
|
+
next_positions = @knight.send(:find_next_positions_at, @knight.current_position)
|
129
|
+
next_positions.sort! # ensure the order is not correct already
|
130
|
+
next_positions.should == [ [0, 2], [2, 2], [3, 3] ]
|
131
|
+
next_positions = @knight.send(:sort_by_warnsdorffs_heuristics, next_positions)
|
132
|
+
next_positions.should == [ [3, 3], [2, 2], [0, 2] ]
|
133
|
+
end
|
134
|
+
|
135
|
+
it "should find next positions, sorted" do
|
136
|
+
next_positions = @knight.find_next_positions
|
137
|
+
next_positions.should == [ [3, 3], [2, 2], [0, 2] ]
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
describe StringResult do
|
142
|
+
it "should show the result correctly for a failed result" do
|
143
|
+
StringResult.new(nil).to_s.should == "No solution found."
|
144
|
+
end
|
145
|
+
|
146
|
+
it "should show the result correctly for a trivial result" do
|
147
|
+
knight = Knight.new([1, 1], [0, 0])
|
148
|
+
knight.instance_variable_set(:@board, [[1]])
|
149
|
+
result = StringResult.new(knight)
|
150
|
+
result.to_s.should == <<-END
|
151
|
+
+--+
|
152
|
+
| 1|
|
153
|
+
+--+
|
154
|
+
END
|
155
|
+
end
|
156
|
+
|
157
|
+
it "should show the result correctly for a non-trivial result" do
|
158
|
+
# in reality, this is not a solvable board size
|
159
|
+
knight = Knight.new([3, 3], [0, 0])
|
160
|
+
knight.instance_variable_set(:@board, [[1, 2, 3], [42, 56, 69], [0, 0, 119]])
|
161
|
+
knight.instance_variable_set(:@steps_taken, 119)
|
162
|
+
StringResult.new(knight).to_s.should == <<-END
|
163
|
+
+----+----+----+
|
164
|
+
| 1| 2| 3|
|
165
|
+
+----+----+----+
|
166
|
+
| 42| 56| 69|
|
167
|
+
+----+----+----+
|
168
|
+
| 0| 0| 119|
|
169
|
+
+----+----+----+
|
170
|
+
END
|
171
|
+
end
|
172
|
+
end
|
metadata
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: tuomas-knights_tour
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.5
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Tuomas Kareinen
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-03-28 00:00:00 -07:00
|
13
|
+
default_executable: knights_tour
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: trollop
|
17
|
+
type: :runtime
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 1.10.0
|
24
|
+
version:
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: rspec
|
27
|
+
type: :development
|
28
|
+
version_requirement:
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 1.2.0
|
34
|
+
version:
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
name: hoe
|
37
|
+
type: :development
|
38
|
+
version_requirement:
|
39
|
+
version_requirements: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: 1.11.0
|
44
|
+
version:
|
45
|
+
description: ""
|
46
|
+
email: tkareine@gmail.com
|
47
|
+
executables:
|
48
|
+
- knights_tour
|
49
|
+
extensions: []
|
50
|
+
|
51
|
+
extra_rdoc_files:
|
52
|
+
- Manifest.txt
|
53
|
+
- CHANGELOG.rdoc
|
54
|
+
- README.rdoc
|
55
|
+
- lib/knights_tour.rb
|
56
|
+
files:
|
57
|
+
- CHANGELOG.rdoc
|
58
|
+
- Manifest.txt
|
59
|
+
- README.rdoc
|
60
|
+
- Rakefile
|
61
|
+
- bin/knights_tour
|
62
|
+
- lib/knights_tour.rb
|
63
|
+
- spec/knights_tour_spec.rb
|
64
|
+
has_rdoc: true
|
65
|
+
homepage: http://github.com/tuomas/knights_tour
|
66
|
+
post_install_message:
|
67
|
+
rdoc_options:
|
68
|
+
- --main
|
69
|
+
- README.rdoc
|
70
|
+
require_paths:
|
71
|
+
- lib
|
72
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - ">="
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: "0"
|
77
|
+
version:
|
78
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: "0"
|
83
|
+
version:
|
84
|
+
requirements: []
|
85
|
+
|
86
|
+
rubyforge_project: searchable-rec
|
87
|
+
rubygems_version: 1.2.0
|
88
|
+
signing_key:
|
89
|
+
specification_version: 2
|
90
|
+
summary: A program that attempts to find a solution to the Knight's Tour problem.
|
91
|
+
test_files: []
|
92
|
+
|