knights_tour 0.3.5
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.rdoc +50 -0
- data/MIT-LICENSE.txt +19 -0
- data/README.rdoc +66 -0
- data/Rakefile +94 -0
- data/bin/knights_tour +40 -0
- data/lib/knights_tour.rb +172 -0
- data/spec/knights_tour_spec.rb +172 -0
- metadata +78 -0
data/CHANGELOG.rdoc
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
=== 0.3.5 released 2009-09-16
|
2
|
+
|
3
|
+
* Display proper version info on Ruby 1.8 when using option "-v".
|
4
|
+
|
5
|
+
=== 0.3.4 released 2009-09-03
|
6
|
+
|
7
|
+
* Slight documentation improvements.
|
8
|
+
* Removed dependencies to Rubygems.
|
9
|
+
|
10
|
+
=== 0.3.3 released 2009-04-17
|
11
|
+
|
12
|
+
* Added missing Rakefile to the distribution gem.
|
13
|
+
* Removed rspec as a dependency gem.
|
14
|
+
|
15
|
+
=== 0.3.2 released 2009-03-31
|
16
|
+
|
17
|
+
* Slight documentation improvements.
|
18
|
+
|
19
|
+
=== 0.3.1 released 2009-03-28
|
20
|
+
|
21
|
+
* Fixed usage documentation.
|
22
|
+
|
23
|
+
=== 0.3.0 released 2009-03-28
|
24
|
+
|
25
|
+
* The software is available as a RubyGem from GitHub.
|
26
|
+
|
27
|
+
=== 0.2.5 released 2009-01-14
|
28
|
+
|
29
|
+
* Small code beautifications.
|
30
|
+
|
31
|
+
=== 0.2.4 released 2008-12-10
|
32
|
+
|
33
|
+
* Small code optimization.
|
34
|
+
|
35
|
+
=== 0.2.3 released 2008-12-11
|
36
|
+
|
37
|
+
* Small documentation improvements.
|
38
|
+
* Refactored code.
|
39
|
+
|
40
|
+
=== 0.2.2 released 2008-12-10
|
41
|
+
|
42
|
+
* Small socumentation improvements.
|
43
|
+
|
44
|
+
=== 0.2.1 released 2008-12-10
|
45
|
+
|
46
|
+
* Refactored code.
|
47
|
+
|
48
|
+
=== 0.2.0 released 2008-12-10
|
49
|
+
|
50
|
+
* First revision that solves the problem correctly. :)
|
data/MIT-LICENSE.txt
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (c) 2008-2009 Tuomas Kareinen.
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,66 @@
|
|
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 Knight's Tour as a RubyGem from GitHub:
|
16
|
+
|
17
|
+
$ sudo gem install tuomas-knights_tour --source http://gems.github.com
|
18
|
+
|
19
|
+
The program is compatible with Ruby 1.9.1.
|
20
|
+
|
21
|
+
== Usage
|
22
|
+
|
23
|
+
In the command line, run the program by entering
|
24
|
+
|
25
|
+
$ knights_tour
|
26
|
+
|
27
|
+
The command attempts to solve the problem on a board of size 8x8, the knight
|
28
|
+
located initially at position 0,0 (the top-left corner). If the program
|
29
|
+
finds a solution, it displays a result similar to the following:
|
30
|
+
|
31
|
+
+---+---+---+---+---+---+---+---+
|
32
|
+
| 1| 62| 13| 36| 3| 38| 31| 28|
|
33
|
+
+---+---+---+---+---+---+---+---+
|
34
|
+
| 14| 35| 2| 63| 32| 29| 4| 39|
|
35
|
+
+---+---+---+---+---+---+---+---+
|
36
|
+
| 61| 12| 59| 34| 37| 42| 27| 30|
|
37
|
+
+---+---+---+---+---+---+---+---+
|
38
|
+
| 50| 15| 64| 43| 58| 33| 40| 5|
|
39
|
+
+---+---+---+---+---+---+---+---+
|
40
|
+
| 11| 60| 49| 54| 41| 24| 45| 26|
|
41
|
+
+---+---+---+---+---+---+---+---+
|
42
|
+
| 16| 51| 18| 57| 44| 55| 6| 23|
|
43
|
+
+---+---+---+---+---+---+---+---+
|
44
|
+
| 19| 10| 53| 48| 21| 8| 25| 46|
|
45
|
+
+---+---+---+---+---+---+---+---+
|
46
|
+
| 52| 17| 20| 9| 56| 47| 22| 7|
|
47
|
+
+---+---+---+---+---+---+---+---+
|
48
|
+
|
49
|
+
The command above is the same as invoking the program with
|
50
|
+
|
51
|
+
$ knights_tour -s 0,0 8,8
|
52
|
+
|
53
|
+
The size of the board and the start position of the knight are configurable,
|
54
|
+
however. For all the options, see
|
55
|
+
|
56
|
+
$ knights_tour -h
|
57
|
+
|
58
|
+
== Contacting
|
59
|
+
|
60
|
+
Please send feedback by email to Tuomas Kareinen < tkareine (at) gmail (dot)
|
61
|
+
com >.
|
62
|
+
|
63
|
+
== Legal notes
|
64
|
+
|
65
|
+
Copyright (c) 2008-2009 Tuomas Kareinen. See MIT-LICENSE.txt in this
|
66
|
+
directory.
|
data/Rakefile
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "lib"))
|
2
|
+
|
3
|
+
require "rubygems"
|
4
|
+
|
5
|
+
full_name = "Knight's Tour"
|
6
|
+
package_name = "knights_tour"
|
7
|
+
require "#{package_name}"
|
8
|
+
version = KnightsTour::VERSION
|
9
|
+
|
10
|
+
require "rake/clean"
|
11
|
+
|
12
|
+
require "rake/gempackagetask"
|
13
|
+
spec = Gem::Specification.new do |s|
|
14
|
+
s.name = package_name
|
15
|
+
s.version = version
|
16
|
+
s.homepage = "http://github.com/tuomas/knights_tour"
|
17
|
+
s.summary = "Solves Knight's Tour problem."
|
18
|
+
s.description = "A program that attempts to find a solution to the Knight's Tour problem."
|
19
|
+
|
20
|
+
s.author = "Tuomas Kareinen"
|
21
|
+
s.email = "tkareine@gmail.com"
|
22
|
+
|
23
|
+
s.platform = Gem::Platform::RUBY
|
24
|
+
s.files = FileList["Rakefile", "MIT-LICENSE.txt", "*.rdoc", "bin/**/*", "lib/**/*", "spec/**/*"].to_a
|
25
|
+
s.executables = ["knights_tour"]
|
26
|
+
|
27
|
+
s.add_dependency("trollop", ">= 1.10.0")
|
28
|
+
|
29
|
+
s.has_rdoc = true
|
30
|
+
s.extra_rdoc_files = FileList["MIT-LICENSE.txt", "*.rdoc"].to_a
|
31
|
+
s.rdoc_options << "--title" << "#{full_name} #{version}" \
|
32
|
+
<< "--main" << "README.rdoc" \
|
33
|
+
<< "--exclude" << "spec" \
|
34
|
+
<< "--line-numbers"
|
35
|
+
end
|
36
|
+
|
37
|
+
Rake::GemPackageTask.new(spec) do |pkg|
|
38
|
+
pkg.need_zip = false
|
39
|
+
pkg.need_tar = true
|
40
|
+
end
|
41
|
+
|
42
|
+
desc "Generate a gemspec file"
|
43
|
+
task :gemspec do
|
44
|
+
File.open("#{spec.name}.gemspec", "w") do |f|
|
45
|
+
f.write spec.to_ruby
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
task :install => [:package] do
|
50
|
+
sh %{sudo gem install pkg/#{package_name}-#{version}.gem}
|
51
|
+
end
|
52
|
+
|
53
|
+
task :uninstall => [:clean] do
|
54
|
+
sh %{sudo gem uninstall #{package_name}}
|
55
|
+
end
|
56
|
+
|
57
|
+
require "rake/rdoctask"
|
58
|
+
desc "Create documentation"
|
59
|
+
Rake::RDocTask.new(:rdoc) do |rd|
|
60
|
+
rd.rdoc_dir = "rdoc"
|
61
|
+
rd.title = "#{full_name} #{version}"
|
62
|
+
rd.main = "README.rdoc"
|
63
|
+
rd.rdoc_files.include("MIT-LICENSE.txt", "*.rdoc", "lib/**/*.rb")
|
64
|
+
rd.options << "--line-numbers"
|
65
|
+
end
|
66
|
+
|
67
|
+
require "spec/rake/spectask"
|
68
|
+
desc "Run specs"
|
69
|
+
Spec::Rake::SpecTask.new(:spec) do |t|
|
70
|
+
t.spec_files = FileList["spec/**/*_spec.rb"]
|
71
|
+
t.spec_opts = ["--colour --format progress --loadby mtime"]
|
72
|
+
t.warning = true
|
73
|
+
t.libs << "lib"
|
74
|
+
end
|
75
|
+
|
76
|
+
desc "Run specs with RCov"
|
77
|
+
Spec::Rake::SpecTask.new(:rcov) do |t|
|
78
|
+
t.spec_files = FileList["spec/**/*.rb"]
|
79
|
+
t.rcov = true
|
80
|
+
t.rcov_opts = ["--exclude", "spec"]
|
81
|
+
t.libs << "lib"
|
82
|
+
end
|
83
|
+
|
84
|
+
desc "Find code smells"
|
85
|
+
task :roodi do
|
86
|
+
sh %{roodi "**/*.rb"}
|
87
|
+
end
|
88
|
+
|
89
|
+
desc "Search unfinished parts of source code"
|
90
|
+
task :todo do
|
91
|
+
FileList["**/*.rb", "**/*.rdoc", "**/*.txt"].egrep /(TODO|FIXME)/
|
92
|
+
end
|
93
|
+
|
94
|
+
task :default => :spec
|
data/bin/knights_tour
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "trollop"
|
4
|
+
require "knights_tour"
|
5
|
+
|
6
|
+
# Parse the command line arguments and invoke the application.
|
7
|
+
|
8
|
+
include KnightsTour
|
9
|
+
|
10
|
+
options = Trollop::options do
|
11
|
+
version "#{File.basename($0)} #{KnightsTour::VERSION}"
|
12
|
+
|
13
|
+
banner <<-EOS
|
14
|
+
A program that attempts to find a solution to the Knight's Tour problem.
|
15
|
+
|
16
|
+
Usage:
|
17
|
+
|
18
|
+
#{File.basename($0)} [OPTIONS] [SIZE]
|
19
|
+
|
20
|
+
Where SIZE (default: 8,8) sets the size of the board to ROWS x COLS.
|
21
|
+
|
22
|
+
Options:
|
23
|
+
EOS
|
24
|
+
|
25
|
+
opt :start_at, "Set the initial position of the knight (default: 0,0)",
|
26
|
+
:type => :string,
|
27
|
+
:required => false
|
28
|
+
end
|
29
|
+
|
30
|
+
begin
|
31
|
+
app_params = {}
|
32
|
+
app_params[:size] = ARGV.shift unless ARGV.empty?
|
33
|
+
app_params[:start_at] = options[:start_at] if options[:start_at]
|
34
|
+
|
35
|
+
app = Application.new(app_params)
|
36
|
+
rescue ArgumentError => e
|
37
|
+
Trollop::die e.to_s
|
38
|
+
end
|
39
|
+
|
40
|
+
puts app.solve
|
data/lib/knights_tour.rb
ADDED
@@ -0,0 +1,172 @@
|
|
1
|
+
module KnightsTour
|
2
|
+
VERSION = "0.3.5"
|
3
|
+
|
4
|
+
class Application
|
5
|
+
def initialize(params = {})
|
6
|
+
@board_size = parse_board_size(params[:size] || [8, 8])
|
7
|
+
@knight_starts_at = parse_position_on_board(
|
8
|
+
params[:start_at] || [0, 0],
|
9
|
+
@board_size)
|
10
|
+
end
|
11
|
+
|
12
|
+
def solve
|
13
|
+
@solution ||= StringResult.new(traverse(
|
14
|
+
Knight.new(@board_size, @knight_starts_at)))
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def parse_pair(param)
|
20
|
+
unless param.is_a?(Array)
|
21
|
+
param = param.to_s.split(",")
|
22
|
+
end
|
23
|
+
[param[0].to_i, param[1].to_i]
|
24
|
+
end
|
25
|
+
|
26
|
+
def parse_board_size(size)
|
27
|
+
size = parse_pair(size)
|
28
|
+
unless size[0] > 0 && size[1] > 0
|
29
|
+
raise ArgumentError,
|
30
|
+
"Board size must be a pair of positive (non-zero) " \
|
31
|
+
"integers, separated by a comma"
|
32
|
+
end
|
33
|
+
size
|
34
|
+
end
|
35
|
+
|
36
|
+
def parse_position_on_board(position, board_size)
|
37
|
+
position = parse_pair(position)
|
38
|
+
unless (0...board_size[0]).include?(position[0]) &&
|
39
|
+
(0...board_size[1]).include?(position[1])
|
40
|
+
raise ArgumentError,
|
41
|
+
"Position must be a pair of positive integers within the " \
|
42
|
+
"size limits of the board, separated by a comma " \
|
43
|
+
"(for example, 0,5 is acceptable for board size 6,6)"
|
44
|
+
end
|
45
|
+
position
|
46
|
+
end
|
47
|
+
|
48
|
+
# Traverse the knight on the board.
|
49
|
+
#
|
50
|
+
# The algorithm is a recursive backtracking search for a first solution
|
51
|
+
# to the problem. The board is copied and modified by moving the knight
|
52
|
+
# to a new position in each recursive step of the algorithm, instead of
|
53
|
+
# modifying a single shared board in place.
|
54
|
+
def traverse(knight)
|
55
|
+
unless knight.traversed?
|
56
|
+
next_positions = knight.find_next_positions
|
57
|
+
next_positions.each do |next_position|
|
58
|
+
knight = traverse(knight.dup.traverse_to(next_position))
|
59
|
+
unless knight.nil?
|
60
|
+
return knight # return the first solution found
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
knight # no solutions found, or already found one
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
class Knight
|
70
|
+
## as [x, y] pairs
|
71
|
+
LEGAL_STEPS = [ [-2, 1], [-1, 2], [ 1, 2], [ 2, 1],
|
72
|
+
[ 2, -1], [ 1, -2], [-1, -2], [-2, -1] ]
|
73
|
+
|
74
|
+
attr_reader :board, :steps_taken, :current_position
|
75
|
+
|
76
|
+
def initialize(board_size, start_at)
|
77
|
+
@board = Array.new(board_size[0]) { Array.new(board_size[1], 0) }
|
78
|
+
@steps_taken = 0
|
79
|
+
traverse_to(start_at)
|
80
|
+
end
|
81
|
+
|
82
|
+
def initialize_copy(other)
|
83
|
+
@board = Marshal.load(Marshal.dump(other.board))
|
84
|
+
@steps_taken = other.steps_taken
|
85
|
+
end
|
86
|
+
|
87
|
+
def traversed?
|
88
|
+
last_step = @board.size * @board[0].size
|
89
|
+
@steps_taken == last_step
|
90
|
+
end
|
91
|
+
|
92
|
+
def traverse_to(new_position)
|
93
|
+
@steps_taken += 1
|
94
|
+
@current_position = new_position
|
95
|
+
@board[@current_position[0]][@current_position[1]] = @steps_taken
|
96
|
+
self
|
97
|
+
end
|
98
|
+
|
99
|
+
def find_next_positions
|
100
|
+
sort_by_warnsdorffs_heuristics(find_next_positions_at(@current_position))
|
101
|
+
end
|
102
|
+
|
103
|
+
private
|
104
|
+
|
105
|
+
# Optimization by applying Warnsdorff's heuristics: attempt to avoid
|
106
|
+
# dead ends by favoring positions with the lowest number of next
|
107
|
+
# available positions (thus, isolated positions become visited first).
|
108
|
+
#
|
109
|
+
# References:
|
110
|
+
# <http://mathworld.wolfram.com/KnightsTour.html>
|
111
|
+
# <http://web.telia.com/~u85905224/knight/eWarnsd.htm>
|
112
|
+
def sort_by_warnsdorffs_heuristics(positions)
|
113
|
+
positions.sort_by do |position|
|
114
|
+
find_next_positions_at(position).size
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def find_next_positions_at(position)
|
119
|
+
positions = LEGAL_STEPS.map do |step|
|
120
|
+
position_after_step(position, step)
|
121
|
+
end
|
122
|
+
positions.reject { |pos| pos.nil? || (@board[pos[0]][pos[1]] > 0) }
|
123
|
+
end
|
124
|
+
|
125
|
+
def position_after_step(from, step)
|
126
|
+
x_pos = from[0] + step[0]
|
127
|
+
y_pos = from[1] + step[1]
|
128
|
+
|
129
|
+
if (0...@board.size).include?(x_pos) &&
|
130
|
+
(0...@board[0].size).include?(y_pos)
|
131
|
+
[x_pos, y_pos]
|
132
|
+
else
|
133
|
+
nil
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
class StringResult
|
139
|
+
def initialize(result)
|
140
|
+
if result.is_a?(Knight)
|
141
|
+
@result = board_to_s(result.board, result.steps_taken)
|
142
|
+
else
|
143
|
+
@result = "No solution found."
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def to_s
|
148
|
+
@result
|
149
|
+
end
|
150
|
+
|
151
|
+
private
|
152
|
+
|
153
|
+
def board_to_s(board, steps_taken)
|
154
|
+
square_width = steps_taken.to_s.length + 1
|
155
|
+
separator_str = separator(square_width, board[0].size)
|
156
|
+
|
157
|
+
output = ""
|
158
|
+
|
159
|
+
board.each do |row|
|
160
|
+
output << separator_str
|
161
|
+
row_output = row.map { |step| "%#{square_width}s" % step }.join("|")
|
162
|
+
output << "|#{row_output}|\n"
|
163
|
+
end
|
164
|
+
|
165
|
+
output << separator_str
|
166
|
+
end
|
167
|
+
|
168
|
+
def separator(board_width, cols)
|
169
|
+
("+" << "-" * board_width) * cols << "+\n"
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
@@ -0,0 +1,172 @@
|
|
1
|
+
require "knights_tour"
|
2
|
+
|
3
|
+
include KnightsTour
|
4
|
+
|
5
|
+
describe KnightsTour::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 KnightsTour::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 KnightsTour::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,78 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: knights_tour
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.3.5
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Tuomas Kareinen
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-10-12 00:00:00 +03:00
|
13
|
+
default_executable:
|
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
|
+
description: A program that attempts to find a solution to the Knight's Tour problem.
|
26
|
+
email: tkareine@gmail.com
|
27
|
+
executables:
|
28
|
+
- knights_tour
|
29
|
+
extensions: []
|
30
|
+
|
31
|
+
extra_rdoc_files:
|
32
|
+
- MIT-LICENSE.txt
|
33
|
+
- CHANGELOG.rdoc
|
34
|
+
- README.rdoc
|
35
|
+
files:
|
36
|
+
- Rakefile
|
37
|
+
- MIT-LICENSE.txt
|
38
|
+
- CHANGELOG.rdoc
|
39
|
+
- README.rdoc
|
40
|
+
- bin/knights_tour
|
41
|
+
- lib/knights_tour.rb
|
42
|
+
- spec/knights_tour_spec.rb
|
43
|
+
has_rdoc: true
|
44
|
+
homepage: http://github.com/tuomas/knights_tour
|
45
|
+
licenses: []
|
46
|
+
|
47
|
+
post_install_message:
|
48
|
+
rdoc_options:
|
49
|
+
- --title
|
50
|
+
- Knight's Tour 0.3.5
|
51
|
+
- --main
|
52
|
+
- README.rdoc
|
53
|
+
- --exclude
|
54
|
+
- spec
|
55
|
+
- --line-numbers
|
56
|
+
require_paths:
|
57
|
+
- lib
|
58
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: "0"
|
63
|
+
version:
|
64
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: "0"
|
69
|
+
version:
|
70
|
+
requirements: []
|
71
|
+
|
72
|
+
rubyforge_project:
|
73
|
+
rubygems_version: 1.3.5
|
74
|
+
signing_key:
|
75
|
+
specification_version: 3
|
76
|
+
summary: Solves Knight's Tour problem.
|
77
|
+
test_files: []
|
78
|
+
|