1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
|
#!/Users/colincrain/perl5/perlbrew/perls/perl-5.32.0/bin/perl
#
# night-moves.pl
#
# Adventure of Knight
# Submitted by: Cheok-Yin Fung
# A knight is restricted to move on an 8×8 chessboard. The knight is
# denoted by N and its way of movement is the same as what it is defined
# in Chess. * represents an empty square. x represents a square with
# treasure.
#
# The Knight’s movement is unique. It may move two squares vertically
# and one square horizontally, or two squares horizontally and one
# square vertically (with both forming the shape of an L).
#
# There are 6 squares with treasures.
#
# Write a script to find the path such that Knight can capture all
# treasures. The Knight can start from the top-left square.
#
# a b c d e f g h
# 8 N * * * * * * * 8
# 7 * * * * * * * * 7
# 6 * * * * x * * * 6
# 5 * * * * * * * * 5
# 4 * * x * * * * * 4
# 3 * x * * * * * * 3
# 2 x x * * * * * * 2
# 1 * x * * * * * * 1
# a b c d e f g h
#
# BONUS: If you believe that your algorithm can output one of the
# shortest possible path.
#
# method:
# just went of it and started writing functions. Read the board from
# DATA, leaving xs and undefining stars. Established all 8 knight
# moves as deltas, then a function that when given a square as a
# tuple of coordinates returns positions the knight can move to
# next. Then a recursive function to start trying paths. Ok, not all
# paths as we don't allow going back to the square we just left
# unless there's no other move, to avoid immediate loops. But what
# about larger loops?
#
# Got the recursive solver sort of working, enough to figure out it
# was rapidly falling into a 6-part cycle. If we were to keep a
# lookup table for moves made from one square to the next, and
# disallow, or at least avoid, repeating the last move made from a
# given position that *might* work. But we're rather far afield now
# and that might not be enough. It might also eliminate solutions;
# it's hard to say and I don't like that. In the end we're just sort
# of wandering around hoping to snatch up the treasures. It seems
# less like our knight is off to slay a dragon and more like he's
# driving aimlessly looking for America. In the fourth act he'll
# discover it was with him the whole time.
# I'm tempted to implement this no-repeating loop-breaker hash but I
# think the strategy is doomed from the outset. I wonder what it
# would look like? Do we exclude the last move or every move,
# somehow? The idea is both intreaging and a total hack.
#
# Alternately we could take a more directed approach. We could
# determine the closest treasure and home in on it, snatch it and
# then move to the next. This would involve more familiarity with
# the game of chess that I can just whip out of local memory. But I
# suppose I could figure it out.
#
# I don't really do chess; never have. I know the rules, how to play, but
# never pursued any deep understanding of the game. I suppose I'll just
# need to make it up.
# If we make a distance algorithm we can find which of the available
# moves gets us closest to the target, then repeat. A little
# analysis determines we can generally employ some stock patterns to
# move one square diagonally, or orthogonally. This sounds like a
# bother to configure but maybe not too hard.
# I wish I had a chess board to make working some of this stuff out a
# little easier. There may be one in the back. The basic patterns would
# be more familiar to someone with a lot of experience in the game. OTOH
# fresh eyes can often see patterns for what they are, rather then
# what's common knowledge. Forward!
#
# new algorithm:
# * select closest treasure by cart dist
# * loop (select):
# * d > 2.5 approach with best move
# * d > 2.1 capture
# * d > 1.4 2-space ortho
# * d > 1 1-space diag
# * d > 0 1-space ortho
# * d == 0 remove treasure from board and select next target
#
#
# © 2021 colin crain
## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ##
use warnings;
use strict;
use utf8;
use feature ":5.26";
use feature qw(signatures);
no warnings 'experimental::signatures';
use constant { START => 'a8' };
## we have four package variables:
## 1. the current location of the knight
## 2. the path of squares the knight has already travelled
## 3. the treasure map, a hash of treasure squares
## 4. a list of the loot spot we've collected
our $knight = START;
our $quest = [ START ];
our $treasures = get_treasure_map();
our $loot = [ ];
search_for_treasure();
###
### MAIN LOOP
###
sub search_for_treasure () {
## see note in the distance() sub on specific distance values
QUEST: while ( 1 ) {
my ($target, $dist) = select_treasure();
last QUEST if not defined $target;
while ( $dist > 0 ) {
if ($dist == 1) { $dist = one_sq_ortho($target) }
elsif ($dist == 2) { $dist = one_sq_diag($target) }
elsif ($dist == 4) { $dist = two_sq_ortho($target) }
else { $dist = approach($target) }
}
## take the treasure
push $loot->@*, $target;
delete $treasures->{ $target };
undef $target;
}
say "quest complete!\n";
say "the knight's quest took ", scalar $quest->@*, " steps";
say "quest path: \n\t", join ' - ', $quest->@*;
say "loot piles found: \n\t", join ' - ', $loot->@*;
}
####
#### DISPATCHES
####
sub approach ($target) {
## make the best move towards the target. All available moves from the current
## position are evaluated and the move resulting in the shortest remaining
## distance to the target is chosen.
my @kpt = chess2mat( $knight )->@*;
my @moves = map { mat2chess( $_ ) }
grep { on_board( $_ ) }
map { add_pt( $_, chess2mat( $knight ) ) }
knight_move_deltas() ;
my $dist;
($knight, $dist) = select_closest( $target, \@moves );
push $quest->@*, $knight;
return $dist;
}
sub two_sq_ortho ($target) {
## to close two squares orthogonally we move one square, half-way, closer and
## dog-leg either left or right two squares. Then the mirror of this move will
## reverse the dog-leg and move forward the additional square. For every pair of
## squares on the board either the left or right movement will remain on the
## board.
my $deltas = point_pair_deltas( $target ) ;
my @laterals = sub { my @half = map { $_ / 2 } $_[0]->@*;
my @out = ( [@half], [@half] );
for ( (0,1) ) {
do { $out[0]->[$_] = 2;
$out[1]->[$_] = -2 } if not $_[0]->[$_];
}
return @out;
} -> ($deltas);
my @moves = map { mat2chess( $_ ) }
grep { on_board( $_ ) }
map { add_pt( $_, chess2mat( $knight ) ) }
@laterals;
push $quest->@*, $moves[0], $target;
$knight = $target;
return 0;
}
sub one_sq_diag ($target) {
## to close one square diagonally there are a pair of squares of the opposite
## color 0.5 square forward and 1.5 squares to the left or right, analogous to
## the "half-way closer and split left or right" algorithm to close two squares
## orthogonally. This works in all cases except the last two squares in the corners
## on the major and minor diagonals, which require special cases
my $deltas = point_pair_deltas( $target ) ;
my @moves = map { mat2chess( $_ ) }
grep { on_board( $_ ) }
map { add_pt( $_, chess2mat( $knight ) ) }
map { mul_pt( $_, $deltas ) }
( [-1,2], [2,-1] );
if ( @moves ) {
push $quest->@*, $moves[0], $target;
$knight = $target ;
return 0;
}
## if there are no viable half-way split positions on the board we're in a
## corner and will need to get out first before we can close.
## there are 2 cases:
## 1. moving diagonally out from a corner square
## to move out we take either valid move to one square beyond
## the target. Then we are one square orthogonal to the target.
## 2. moving diagonally into a corner square
## to move into a corner we make one lateral diagonal step. Then
## we are a two-square orthogonal distance from the target
if ($knight =~ /a8|a1|h8|h1/) { ## case (1) moving out
approach( $target );
one_sq_ortho( $target );
}
else { ## case (2) moving in
my @lateral_delta = $deltas->@*;
$lateral_delta[0] *= -1; ## change direction 90°
my $temp_target = mat2chess( add_pt( \@lateral_delta, chess2mat($knight) ) );
one_sq_diag( $temp_target );
two_sq_ortho( $target );
}
return 0;
}
sub one_sq_ortho ($target) {
## to close one square orthogonally, we first need to move forward into the
## target's rank, to either square two squares to the left or right as we fit on
## the board. The problem then becomes a two-square ortogonal close. Every pair
## of squares will have one of the two lateral position available, then the
## 2-ortho close works in all cases
my $deltas = point_pair_deltas( $target ) ;
my $laterals = sub { my @out = ( [$_[0]->@*], [$_[0]->@*] );
for ( (0,1) ) {
do { $out[0]->[$_] = 2;
$out[1]->[$_] = -2 } if not $_[0]->[$_];
}
return @out;
};
my @laterals = $laterals->($deltas);
my @moves = map { mat2chess( $_ ) }
grep { on_board( $_ ) }
map { add_pt( $_, chess2mat( $knight ) ) }
@laterals;
push $quest->@*, $moves[0];
$knight = $moves[0] ;
two_sq_ortho ($target) ;
return 0;
}
###
### HELPERS
###
sub add_pt ($pt1, $pt2) {
## sums point coordinates
return [ $pt1->[0]+$pt2->[0], $pt1->[1]+$pt2->[1] ];
}
sub mul_pt ($pt1, $pt2) {
## multiplies point coordinates
return [ $pt1->[0]*$pt2->[0], $pt1->[1]*$pt2->[1] ];
}
sub on_board( $pt ) {
## boolean for whether a point is on the chessboard
return 0 <=
|