Pen and Paper (#90)

w00t!

I finally got it. At first I misunderstood the algorithm, I was counting
the
number of free adjacent cells, and giving preference to the move with
the
least amount. This actually made the script take longer than when I was
picking the move randomly. Finally I figured out we were scoring the
least
amount of possible moves (per move)…I also refactored the code into a
class.

So here are a few timings:
$ time ruby pnp.rb 15 > /dev/null
real 0m0.196s
user 0m0.092s
sys 0m0.009s
$ time ruby pnp.rb 25 > /dev/null
real 0m0.460s
user 0m0.334s
sys 0m0.036s
$ time ruby pnp.rb 50 > /dev/null
real 0m0.801s
user 0m0.659s
sys 0m0.052s
$ time ruby pnp.rb 100 > /dev/null
real 0m5.472s
user 0m5.038s
sys 0m0.299s

Nothing to be thrilled about, that’s fer shur.

And the code:
#####################################
#!/usr/bin/ruby

PnP.rb :: quiz no.90

class SolveIt
def initialize(x,y,gridsize)
@gridsize = gridsize
@coords = [x,y]

@moves_offset = {
  "l"  => [0,-3],
  "r"  => [0,3],
  "u"  => [-3,0],
  "d"  => [3,0],
  "ur" => [-2,2],
  "ul" => [-2,-2],
  "dr" => [2,2],
  "dl" => [2,-2]
}

@matrix = Array.new(@gridsize) { Array.new(@gridsize) { "." } }
@matrix[@coords[0]][@coords[1]] = 1
@totalnums = @gridsize*@gridsize
@nextint = 2

end

def cell_free?(x,y)
if x >= 0 && x < @gridsize && y >= 0 && y < @gridsize &&
@matrix[x][y] == “.”
return true
else
return false
end
end

def num_moves(m)
moves = 0
x,y = @moves_offset[m]
@moves_offset.each do |k,v|
moves += 1 if cell_free?(@coords[0]+x+v[0],@coords[1]+y+v[1])
end
moves
end

def check_moves_and_return_best_one
moves = []
@moves_offset.each do |k,v|
moves << k if cell_free?(@coords[0]+v[0],@coords[1]+v[1])
end
if moves.length == 0
return nil
elsif moves.length ==1
return moves[0]
end
score = {}
moves.each do |m|
score[m] = num_moves(m)
end
score = score.invert.sort
return score[0][1]
end

def print_matrix
@matrix.each do |row|
row.each do |cell|
print " %3s " % cell
end
print “\n”
end
end

def do_it
@totalnums.times do
move = check_moves_and_return_best_one
if move == nil
break # try again
end
x,y = @moves_offset[move]
@coords[0] += x
@coords[1] += y
@matrix[@coords[0]][@coords[1]] = @nextint
@nextint += 1
if @nextint == @totalnums + 1
print_matrix
exit
end
end
end
end

while 1:
gridsize = ARGV[0].to_i
x, y = rand(gridsize), rand(gridsize)
it = SolveIt.new(x,y,gridsize)
it.do_it
end
#############################################
-d

Here’s my solution. It starts in the middle of the board and hops to
one of the next spaces that has a large amount of moves that can be
made from there. It’s not all that fast though, I didn’t really spend
too much time optimizing.

Mitchell Koch

Here’s my solution for the nonce .

I’ve probably been focusing more on packaging this and less on
optimizing it than I should.

The approach is a recursive search.
0. Place the first number

  1. Pick the best available direction and place the next number there.
  2. Try to place the rest of the numbers (by recursive call to step 2)
  3. If 2 fails , remove the number placed in step 1, pick the next
    direction and try again.
  4. If we run out of directions, report failure to the calling recursion.

Picking the best available direction is based on Eliott’s suggested
heuristic of trying to use squares which have less available next
steps first.

I did this in two files. pen_and_paper.rb is the main program, it
does parameter parsing, and can benchmark or profile the code as
dictated by the parameters. Right now, it only searches for a
solution starting in row 0, column 1. This should be a parameter and
I should also try alternatives if the starting position doesn’t work.

The second file ruby_quiz_90.rb is a module containing the classes
Board, Player and Move.

Most of the time actually still seems to be spent in checking whether
a square is occupied or not. My initial stab at this did bounds
checking since ruby Arrays just return nil for out of bounds indices.
The current iteration uses 0 instead of nil in empty cells and uses
guard rows above and below the board in order to avoid bounds
checking. I might next try eliminating the guard rows and just define
[] for NilClass to return nil.

Anyway, here’s what I’ve got now
=== pen_and_paper.rb ===
require ‘benchmark’
include Benchmark

require ‘optparse’

require ‘ruby_quiz_90’

board_size = 5
benchmark = false
profile = false
iterations = 1

opts = OptionParser.new
opts.on(‘-sBOARDSIZE’,
‘-s BOARDSIZE’,
‘–size=BOARDSIZE’,
‘–size BOARDSIZE’,
‘Height and width of board, default is 5x5’,
Integer) {|val| board_size = val }
opts.on(‘–bench’,
‘Specifies that a benchmark will be done.’
) { | val | benchmark = true }

opts.on(‘–profile’,
‘Profile execution’) { profile = true}

opts.on(‘–iterations=COUNT’,
‘–iterations COUNT’,
‘COUNT specifies the number of iterations’,
‘for benchmarking or profiling’,
Integer) { | val | iterations = val }

opts.on(‘–help’,
‘produce this help text’) { puts opts.to_s
exit }

begin
opts.parse(ARGV)
rescue
puts opts.to_s
exit
end

puts “board_size is #{board_size}x#{board_size}”
puts “benchmark=#{benchmark}, profile=#{profile},
iterations=#{iterations}”

player = nil

if benchmark
bm(iterations) do | test |
test.report(“#{board_size}x#{board_size}:”) do
player = RubyQuiz90::Player.new(board_size)
player.play(0,1)
end
end
else
if profile
require ‘profile’
end
iterations.times do
player = RubyQuiz90::Player.new(board_size)
player.play(0,1)
end
end
puts player.board
=== ruby_quiz_90.rb ===
module RubyQuiz90
class Board

	# Changes:
	#    pad row with 3 guard rows above and below
	#      this allows available? to avoid having to catch
	#      nil not understanding []
	def initialize(size)
		@row = Array.new(size+6)
	        (0..2).each { | i | @row[i] = Array.new(size) }
		(3..size+2).each { | i | @row[i] = Array.new(size, 0) }
		(size+3..size+5).each { | i | @row[i] = Array.new(size) }
		@size = size

@range = (0…size-1)

	end

	def take(row, col, value)
		@row[row+3][col] = value
	end

	def available?(row, col)
		@row[row+3][col] == 0 rescue false
	end

	def take_back(row, col)
		@row[row+3][col] = 0
	end

	def value(row, col)
		@row[row+3][col]
	end

	def to_s
		elem_width = ((@size * @size).to_s.length) + 1
		header = '+-' + ('-' *(@size * elem_width)) + '+'
		result = header.dup
		(0...@size).each do | i |
			row = @row[i+3]
			result << "\n|"
			row.each do | elem |
			           result << ((elem == 0) ?
					      (" " * elem_width) :
					      sprintf("%#{elem_width}d", elem))
			end
		result << ' |'
		end
		result << "\n"
		result << header
	end

	def self.testboard(s)
		b = new(s)
		(0..s-1).each do | r |
			(0..s-1).each do | c |
			b.take(r, c, c + 1 + (r*s))
			end
		end
		b
	end

end

class Player
	Direction_vectors = [
		[0, 3],   #move east
		[2, 2],   #move southeast
		[3, 0],   #move south
		[2, -2],  #move southwest
		[0, -3],  #move west
		[-2, -2], #move northwest
		[-3, -3], #move north
		[-2, 2],  #move northeast
	]

	#		No_more_moves = Direction_vectors.length
	#
	#		Last_move = (No_more_moves) - 1

	def initialize(size, debug = 2)
		@board = RubyQuiz90::Board.new(size)
		@last = size * size
		@debug = debug
	end

	def play (start_row=nil, start_col=nil)
		row = start_row || rand(@board.size)
		col = start_col || rand(@board.size)
		@board.take(row, col, 1)
		fill_all_from(start_row, start_col)
	end

	def board
		@board
	end

	# Fill the board after the last number was placed at r,c
	# If the board can be filled return true
	# If the board cannot be filled restore the board to its
	# initial state at the time of this call and return false
	def fill_all_from(r, c)

		value = @board.value(r,c) + 1

		puts "Trying to fill board starting with #{value}, from #{r},

#{c}}" if @debug > 2
puts @board if @debug >= 3
return true if value > @last # our work is done!

		#now try to fill the next value
		optimized_moves(r, c).each do | move |
			row = move.row
		        col = move.col
			puts "Trying to move placing #{value} at #{row}, #{col}" if @debug > 

2
@board.take(row, col, value)
puts “Placed #{value} at #{row}, #{col}” if @debug > 2
if fill_all_from(row, col)
return true
else
@board.take_back(row, col)
end
end

		# if we get here, it's time to back out
		puts "All trials failed at #{value}" if @debug > 2
		puts @board if @debug > 2
		return false
	end
            # return a list of moves from row, col optimized so that
	# squares with more possible further moves come first
	def optimized_moves(row, col)
		moves = []
		Direction_vectors.each do | dx, dy |
			r = row + dy
		        c = col + dx
			if @board.available?(r,c)
				moves << Move.new(r, c, availability_count(r, c))
			end
		end
		moves.sort!
		moves
	end

            # return the number of available squares from row, col
	def availability_count(row, col)
		Direction_vectors.inject(0) do | sum, (dx, dy)|
		       	@board.available?(row+dy, col+dx) ? sum + 1 : sum
		end

	end
end

class Move
	attr_accessor :row, :col, :count
	def initialize(r, c, count)
		@row = r
		@col = c
		@count = count
	end

	# a move is greater (should come later) if it has a lower
	# available move count than another
	def <=>(move)
		count - move.count
	end

	def to_s
		"Count=#{@count}, row=#{@row}, col=#{@col}"
	end
end

end

Rick DeNatale

http://talklikeaduck.denhaven2.com/

On Aug 12, 2006, at 5:15 PM, James Edward G. II wrote:

In all fairness, I really would love to go back and add the solver,
but I am very, very busy this weekend. :frowning: We will see if I can
steal the time.

I found the time. Here are some fun timings with it:

$ time ruby grid_fill.rb -s 19


| 203 229 224 204 230 223 209 233 220 210 234 219 211 235 160 194 236
161 167 |
| 268 174 201 34 173 200 35 172 199 352 171 198 312 170 197 163 169
196 164 |
| 227 205 38 228 225 60 231 222 69 232 221 70 159 218 71 238 166
193 237 |
| 202 33 269 207 36 55 208 353 54 213 311 351 212 310 313 195 309
162 168 |
| 267 175 226 61 39 89 84 59 216 81 68 217 72 75 240 348 74
239 165 |
| 270 206 37 56 97 65 53 98 66 354 99 79 158 350 78 307 314
192 308 |
| 49 32 40 90 85 58 215 88 83 214 156 82 241 361 73 242 333
347 243 |
| 266 176 110 62 52 111 128 64 116 80 67 355 100 76 319 349 77
306 315 |
| 271 91 50 57 96 134 86 107 155 87 342 3 157 343 4 360 317
191 334 |
| 48 31 41 112 129 63 115 130 127 17 119 104 320 1 103 303 332
346 244 |
| 265 177 109 135 51 108 136 133 117 106 154 356 101 6 318 344 5
305 316 |
| 272 92 45 28 95 143 126 18 148 131 341 2 120 302 331 359 283
190 335 |
| 47 30 42 113 124 138 114 123 137 16 118 105 321 357 102 304 329
345 245 |
| 264 178 26 144 44 27 149 132 340 11 153 293 338 7 284 301 336
250 282 |
| 273 93 46 29 94 142 125 19 147 122 322 15 121 297 330 358 254
189 328 |
| 182 24 43 290 25 139 291 12 150 292 339 10 285 294 337 251 281
300 246 |
| 263 179 21 145 261 20 146 141 323 14 152 296 325 8 257 298 327
249 255 |
| 274 289 183 275 288 184 276 287 185 277 286 186 278 252 187 279 253
188 280 |
| 181 23 262 180 22 140 260 13 151 259 324 9 258 295 326 248 256

299 247

real 0m0.013s
user 0m0.007s
sys 0m0.005s
$ time ruby grid_fill.rb -s 19


| 282 308 303 283 309 302 288 312 299 289 313 298 290 314 239 273 315
240 246 |
| 347 253 280 113 252 279 114 251 278 70 250 277 30 249 276 242 248
275 243 |
| 306 284 117 307 304 139 310 301 148 311 300 149 238 297 150 317 245
272 316 |
| 281 112 348 286 115 134 287 71 133 292 29 69 291 28 31 274 27
241 247 |
| 346 254 305 140 118 168 163 138 295 160 147 296 151 154 319 66 153
318 244 |
| 349 285 116 135 176 144 132 177 145 72 178 158 237 68 157 25 32
271 26 |
| 128 111 119 169 164 137 294 167 162 293 235 161 320 79 152 321 51
65 322 |
| 345 255 189 141 131 190 207 143 195 159 146 73 179 155 37 67 156
24 33 |
| 350 170 129 136 175 213 165 186 234 166 60 82 236 61 83 78 35
270 52 |
| 127 110 120 191 208 142 194 209 206 96 198 183 38 80 182 21 50
64 323 |
| 344 256 188 214 130 187 215 212 196 185 233 74 180 85 36 62 84
23 34 |
| 351 171 124 107 174 222 205 97 227 210 59 81 199 20 49 77 1
269 53 |
| 126 109 121 192 203 217 193 202 216 95 197 184 39 75 181 22 47
63 324 |
| 343 257 105 223 123 106 228 211 58 90 232 11 56 86 2 19 54
329 361 |
| 352 172 125 108 173 221 204 98 226 201 40 94 200 15 48 76 333
268 46 |
| 261 103 122 8 104 218 9 91 229 10 57 89 3 12 55 330 360
18 325 |
| 342 258 100 224 340 99 225 220 41 93 231 14 43 87 336 16 45
328 334 |
| 353 7 262 354 6 263 355 5 264 356 4 265 357 331 266 358 332
267 359 |
| 260 102 341 259 101 219 339 92 230 338 42 88 337 13 44 327 335

17 326

real 0m0.012s
user 0m0.007s
sys 0m0.005s
$ time ruby grid_fill.rb -s 19


| 139 165 160 140 166 159 145 169 156 146 170 155 147 171 96 130 172
97 103 |
| 204 110 137 331 109 136 332 108 135 288 107 134 248 106 133 99 105
132 100 |
| 163 141 335 164 161 357 167 158 5 168 157 6 95 154 7 174 102
129 173 |
| 138 330 205 143 333 352 144 289 351 149 247 287 148 246 249 131 245
98 104 |
| 203 111 162 358 336 25 20 356 152 17 4 153 8 11 176 284 10
175 101 |
| 206 142 334 353 33 1 350 34 2 290 35 15 94 286 14 243 250
128 244 |
| 346 329 337 26 21 355 151 24 19 150 92 18 177 297 9 178 269
283 179 |
| 202 112 46 359 349 47 64 361 52 16 3 291 36 12 255 285 13
242 251 |
| 207 27 347 354 32 70 22 43 91 23 278 300 93 279 301 296 253
127 270 |
| 345 328 338 48 65 360 51 66 63 314 55 40 256 298 39 239 268
282 180 |
| 201 113 45 71 348 44 72 69 53 42 90 292 37 303 254 280 302
241 252 |
| 208 28 342 325 31 79 62 315 84 67 277 299 56 238 267 295 219
126 271 |
| 344 327 339 49 60 74 50 59 73 313 54 41 257 293 38 240 265
281 181 |
| 200 114 323 80 341 324 85 68 276 308 89 229 274 304 220 237 272
186 218 |
| 209 29 343 326 30 78 61 316 83 58 258 312 57 233 266 294 190
125 264 |
| 118 321 340 226 322 75 227 309 86 228 275 307 221 230 273 187 217
236 182 |
| 199 115 318 81 197 317 82 77 259 311 88 232 261 305 193 234 263
185 191 |
| 210 225 119 211 224 120 212 223 121 213 222 122 214 188 123 215 189
124 216 |
| 117 320 198 116 319 76 196 310 87 195 260 306 194 231 262 184 192

235 183

real 0m0.012s
user 0m0.007s
sys 0m0.005s

It even always gets the bonus points for the circular solution.

OK, I admit finding circular solutions on the big boards sucks. :wink:

Here’s the code:

#!/usr/bin/env ruby -w

require “enumerator”

class PenAndPaperGame
def self.circular_solutions
@circular ||= if File.exist?(“circular_solutions.dump”)
File.open(“circular_solutions.dump”) { |file| Marshal.load
(file) }
else
Array.new
end
end

def initialize(size)
@size = size
@largest = @size * @size

 @grid = Array.new(@largest)

end

def solve
if self.class.circular_solutions[@size].nil?
solve_manually
else
@grid = self.class.circular_solutions[@size]
offset = @grid[rand(@grid.size)]
@grid.map! { |n| (n + offset) % @largest + 1 }
to_s
end
end

def solve_manually
x, y = rand(@size), rand(@size)
count = mark(x, y)

 loop do
   to = jumps(x, y)
   return self.class.new(@size).solve_manually if to.empty?

   scores    = rate_jumps(to)
   low       = scores.min
   next_jump = to.enum_for(:each_with_index).select do |jump|
     scores[jump.last] == low
   end.sort_by { rand }.first.first

   count = mark(*(next_jump + [count]))
   x, y  = next_jump

   if count > @largest
     if circular?
       self.class.circular_solutions[@size] = @grid
       File.open("circular_solutions.dump", "w") do |file|
         Marshal.dump(self.class.circular_solutions, file)
       end
       return to_s
     else
       puts "Found this solution:"
       puts to_s
       puts "Continuing search for a circular solution..."
       return self.class.new(@size).solve_manually
     end
   end
 end

end

def to_s
width = @largest.to_s.size
border = " -" + (["-" * width] * @size).join("-") + “- \n”

 border +
 @grid.enum_for(:each_slice, @size).inject(String.new) do |grid,

row|
grid + “| " + row.map { |n| n.to_s.center(width) }.join(” “) +
" |\n”
end +
border
end

private

def at(x, y)
x + y * @size
end

def mark(current_x, current_y, mark = 1)
@grid[at(current_x, current_y)] = mark
mark + 1
end

def jumps(from_x, from_y, grid = @grid)
[ [-3, 0],
[3, 0],
[0, -3],
[0, 3],
[2, 2],
[-2, 2],
[2, -2],
[-2, -2] ].map do |jump|
[from_x + jump.first, from_y + jump.last]
end.select do |jump|
jump.all? { |to| (0…@size).include? to } and grid[at
(*jump)].nil?
end
end

def rate_jumps(choices)
choices.map { |jump| jumps(*jump).size }
end

def circular?
grid = @grid.dup
grid[grid.index(@largest)] = nil

 x, y = grid.index(1).divmod(@size).reverse

 not jumps(x, y, grid).empty?

end
end

if FILE == $PROGRAM_NAME
size = ARGV.first && ARGV.first =~ /\A-s(?:ize)?\Z/ ?
ARGV.last.to_i : 5
puts PenAndPaperGame.new(size).solve
end

END

James Edward G. II

Just want to correct a misstatement. The worse-case behavior is
actually O(en2), which better explains why it gets bad so fast.

Regards, Morton

This is my submission for Ruby Q. #90.

My solution’s best feature is that it’s based on a simple search
algorithm and that’s also its worst feature. The randomized depth-
first search I use is easy to understand, was easy to code, and
required almost no debugging. But it has O(e**n) worst-case behavior.
Which means it works well enough for small n, but it quickly becomes
intolerable as n grows. When does it begin to suck? Depends on how
patient you are. I ran out of patience at n = 7.

The search is programmed to terminate as soon as it finds one, cyclic
solution. Of course, from any cyclic solution, you can generate n**2

  • 1 additional cyclic solutions by applying the included Grid#shift
    method for n = 1 to 35.

My solution’s second best feature is that it’s short. Even with a
fair amount of commenting, it comes in at less than 200 lines.

Here are some results from running it with n = 5 and n = 6.

~/Projects/Ruby/Ruby Q. mg: time ./quiz_90.rb

| 1 16 19 2 15 |
| 11 22 5 12 21 |
| 18 8 25 17 7 |
| 4 13 20 3 14 |

10 23 6 9 24

real 0m0.095s
user 0m0.053s
sys 0m0.014s

~/Projects/Ruby/Ruby Q. mg: time ./quiz_90.rb

| 1 21 6 9 20 |
| 14 11 3 17 12 |
| 5 8 25 22 7 |
| 2 18 13 10 19 |

15 23 4 16 24

real 0m0.048s
user 0m0.017s
sys 0m0.011s

~/Projects/Ruby/Ruby Q. mg: time ./quiz_90.rb

| 1 13 21 18 12 |
| 6 16 24 9 4 |
| 22 19 2 14 20 |
| 25 10 5 17 11 |

7 15 23 8 3

real 0m0.166s
user 0m0.108s
sys 0m0.014s

~/Projects/Ruby/Ruby Q. mg: time ./quiz_90.rb

| 1 15 22 36 5 23 |
| 26 9 19 25 10 18 |
| 21 29 6 16 30 35 |
| 2 14 11 33 4 24 |
| 27 8 20 28 7 17 |

12 32 3 13 31 34

real 0m3.004s
user 0m2.827s
sys 0m0.034s

~/Projects/Ruby/Ruby Q. mg: time ./quiz_90.rb

| 1 8 31 36 7 30 |
| 27 13 16 24 12 17 |
| 20 35 2 9 32 3 |
| 15 23 28 14 6 29 |
| 26 10 19 25 11 18 |

21 34 5 22 33 4

real 1m18.337s
user 1m12.836s
sys 0m0.567s

~/Projects/Ruby/Ruby Q. mg: time ./quiz_90.rb

| 1 15 29 36 16 28 |
| 9 6 18 26 7 19 |
| 22 33 11 14 30 35 |
| 2 25 8 5 17 27 |
| 10 13 21 34 12 20 |

23 32 3 24 31 4

real 0m1.126s
user 0m1.027s
sys 0m0.021s

Notice the wide variation in run times. You have to be lucky to get a
really short run. This suggests some interesting statistical questions:

  1. What is the distribution of run times as a function of n?

  2. What is the mean number of backtracking events over all the
    possible searches as a function of n?

  3. What is the probability that a search proceeds to a solution with
    at most k backtracks as a function of n?

I don’t know the answer to any of these questions, but I would like to.

#! /usr/bin/ruby -w # Author: Morton G. # # Date: August 15, 2006 # # Ruby Q. #90 -- Pen and Paper Game

A grid is a matrix of cells.

class Grid

def initialize(dims)
   @rows, @cols = dims
   @size = @rows * @cols
   @grid = Array.new(@rows) {|i| Array.new(@cols) {|j| Cell.new

(i, j)}}
end

# Return a deep copy.
def copy
   result = Grid.new(dimensions)
   result.grid.each_with_index do |row, i|
      row.each_with_index {|cell, j| cell.val = self[i, j].val}
   end
   result
end

# Shifts the values of the cells in the grid by <offset> under the
# constraint that values are folded into the range 1..@size.
def shift!(offset)
   @grid.each do |row|
      row.each do |cell|
         val = (cell.val + offset) % @size
         cell.val = (val == 0 ? @size : val)
      end
   end
   self
end

# Return the dimensions of the grid.
def dimensions
   [@rows, @cols]
end

# Return the cell at positon [row, col].
# Call as <grid-name>[row, col]
def [](*args)
   row, col = args
   @grid[row][col]
end

# Assigns a cell to the positon [row, col].
# Call as <grid-name>[row, col] = cell
def []=(*args)
   row, col, cell = args
   @grid[row][col] = cell
end

# Format the grid as a bordered table.
def to_s
   rule = '-' * (4 * @cols + 4) + "\n"
   grid_str = ""
   @grid.each do |row|
      grid_str << row.inject("|  ") do |row_str, cell|
         row_str << ("%2d  " % cell.val)
      end
      grid_str << "|\n"
   end
   "" << rule << grid_str << rule
end

attr_reader :rows, :cols, :size, :grid

end

A path is an array of cells, where no two cells occupy the same

location

in some grid. A complete path fills the grid.

class Path < Array

# Make a deep copy of a path.
def copy
   result = Path.new
   each {|cell| result << cell.dup}
   result
end

Sequentially number the cells in the path.

def number!
   each_with_index {|cell, i| cell.val = i + 1}
   self
end

# Report whether or not a path is cyclic.
def cyclic?
   p0, p1 = self[0], self[-1]
   delta = [(p1.row - p0.row).abs, (p1.col - p0.col).abs]
   delta == [3, 0] || delta == [0, 3] || delta == [2, 2]
end

# Make a grid from a path.
# Warning: if the path isn't complete, the resulting grid wont't
# represent a solution.
def to_grid(size)
   result = Grid.new([size, size])
   each {|cell| result[cell.row, cell.col] = cell}
   result
end

end

A cell is a simple object that knows its value and its position in

a grid. It also encodes the game’s movement rule.

class Cell

def initialize(row, col, val=0)
   @row, @col = row, col
   @val = val
end

# Return a list of targets -- an array containing all the cells

in the
# grid reachable from this cell in one step.
def targets(grid)
size = grid.rows
result = []
result << grid[@row, @col - 3] if @col - 3 >= 0 # north
result << grid[@row + 2, @col - 2]
if @row + 2 < size && @col - 2 >= 0 # northeast
result << grid[@row + 3, @col] if @row + 3 < size # east
result << grid[@row + 2, @col + 2]
if @row + 2 < size && @col + 2 < size # southeast
result << grid[@row, @col + 3] if @col + 3 < size # south
result << grid[@row - 2, @col + 2]
if @row - 2 >= 0 && @col + 2 < size # southwest
result << grid[@row - 3, @col] if @row - 3 >= 0 # west
result << grid[@row - 2, @col - 2]
if @row - 2 >= 0 && @col - 2 >= 0 # northwest
result
end

attr_accessor :row, :col, :val

end

A solver uses a depth-first search to find one solution for a

square grid # of a given size.
class Solver

def initialize(size)
   @size = size
   @solution = nil
   @test_grid = Grid.new([@size, @size])
   cell = @test_grid[0, 0]
   targets = cell.targets(@test_grid)
   goals = targets.dup
   @backtrack_chain = [[Path.new << cell, targets, goals]]
end

# Return a new link for the backtrack chain if it can be extended;
# otherwise, return nil.
def next_link
   path, targets, goals = @backtrack_chain.last
   return nil if targets.empty? || goals.empty?
   # Here is where the randomization takes place.
   cell = targets[rand(targets.size)]
   next_path = path.dup << cell
   # Restricts target list to accessible cells not already on the

path.
next_targets = cell.targets(@test_grid) - path
next_goals = goals.dup
next_goals.delete(cell) if goals.include?(cell)
# Make sure cell won’t be picked again if backtrack occurs.
targets.delete(cell)
[next_path, next_targets, next_goals]
end

# The algorithm is a randomized depth-first search.
def solve
   final_size = @size * @size
   loop do
      link = next_link
      if link then
         @backtrack_chain.push(link)
      else
         @solution = @backtrack_chain.pop.first
         break if @solution.size == final_size
         if @backtrack_chain.empty? then
            raise(RuntimeError, "No solution for #@size x #@size

grid")
end
end
end
@solution.number!
end

attr_reader :solution

end

SIZE = 5
solver = Solver.new(SIZE)
puts solver.solve.to_grid(SIZE)

Regards, Morton