I'm a new programmer currently coding a javascript alpha-beta pruning minimax algorithm for my chess engine, using Chess.js and Chessboard.js. I've implemented a basic algorithm with move ordering. Currently, it's evaluating around 14000 nodes for 8 seconds, which is way too slow. Is there something wrong with my algorithm or are there optimizations that I haven't implemented? My algorithm can't process anything deeper than depth 4 within reasonable time constraints. Thank you.
P.S. the "tracking Eval" function just evaluates each specific move as a way to avoid doing a full evaluation of boards at leaf nodes, this optimization sped up my program by around 50%, but it's still slow right now.
function minimax(game, depth, distanceFromRoot, alpha, beta, gameEval) {//returns gameEval
if (depth === 0) {
nodeNum++;
if(game.turn() === 'b'){
return (-gameEval / 8);
}else{
return (gameEval / 8);
}
}
// run eval
var prevEval = gameEval;
var moves = game.moves();
moveOrdering(moves);
var bestMove = null;
var bestEval = null;
for (let i = 0; i < moves.length; i++) {
var gameCopy = new Chess()//dummy board to pass down
gameCopy.load(game.fen())
const moveInfo = gameCopy.move(moves[i])
var curGameCopy = new Chess()//static board to eval, before the move so we know which piece was taken if a capture occurs
curGameCopy.load(game.fen())
var curEval = trackingEval(curGameCopy, prevEval, moveInfo, moves[i]); //returns the OBJECTIVE eval for the current move for current move sequence
var evaluated = -minimax(gameCopy, depth - 1, distanceFromRoot + 1, -beta, -alpha, curEval);//pass down the current eval for that move
if (evaluated >= beta) {
return beta;
}
if (evaluated > alpha){
alpha = evaluated
bestMove = moves[i]
bestEval = evaluated;
if (distanceFromRoot === 0) {
bestEval = evaluated;
}
}
}
if(distanceFromRoot === 0){
setEval(-bestEval)
return bestMove;
}
return alpha;
}
It's hard to say what optimizations you have made and what is reasonable since we only see a small part of your code. Your evaluation can be slow, your move ordering can be slow/incorrect, and to copy the board is also slower than to make and then unmake the move.
You can find lots of advice on how to speed up your algorithm here: https://www.chessprogramming.org/Search. Chessprogramming.org is a very good resource for developing your engine in general too.
I see two quick optimizations, before going further into other classical optimization.
Do not compute the evaluation of the board except when depth = 0. I assume that you compute the whole evaluation at every step, it's very time consuming and totally unnecessary.
Do not copy the board each time. It's also time consuming. Work with one board for the whole search, in which you make and unmake moves when you are doing the search. The pseudo-code for this is:
for move in moves:
board.do(move) #the original (not a copy) board has made the move
#Alpha-beta stuff like you did
board.undo(move) #restore the board
Related
I need some help to optimize the code below. I can't understand how to rewrite the code to avoid the deoptimizations.
Below is the code that works very slow on the Node platform. I took it from benchmarksgame-team binary-trees benchmark and added minor changes.
When it is run with --trace-deopt it shows that the functions in the hot path are depotimized, i.e.
[bailout (kind: deopt-lazy, reason: (unknown)): begin. deoptimizing 0x02117de4aa11 <JSFunction bottomUpTree2 (sfi = 000002F24C7D7539)>, opt id 3, bytecode offset 9, deopt exit 17, FP to SP delta 80, caller SP 0x00807d7fe6f0, pc 0x7ff6afaca78d]
The benchmark, run it using node --trace-deopt a 20
function mainThread() {
const maxDepth = Math.max(6, parseInt(process.argv[2]));
const stretchDepth = maxDepth + 1;
const check = itemCheck(bottomUpTree(stretchDepth));
console.log(`stretch tree of depth ${stretchDepth}\t check: ${check}`);
const longLivedTree = bottomUpTree(maxDepth);
for (let depth = 4; depth <= maxDepth; depth += 2) {
const iterations = 1 << maxDepth - depth + 4;
work(iterations, depth);
}
console.log(`long lived tree of depth ${maxDepth}\t check: ${itemCheck(longLivedTree)}`);
}
function work(iterations, depth) {
let check = 0;
for (let i = 0; i < iterations; i++) {
check += itemCheck(bottomUpTree(depth));
}
console.log(`${iterations}\t trees of depth ${depth}\t check: ${check}`);
}
function TreeNode(left, right) {
return {left, right};
}
function itemCheck(node) {
if (node.left === null) {
return 1;
}
return 1 + itemCheck2(node);
}
function itemCheck2(node) {
return itemCheck(node.left) + itemCheck(node.right);
}
function bottomUpTree(depth) {
return depth > 0
? bottomUpTree2(depth)
: new TreeNode(null, null);
}
function bottomUpTree2(depth) {
return new TreeNode(bottomUpTree(depth - 1), bottomUpTree(depth - 1))
}
console.time();
mainThread();
console.timeEnd();
(V8 developer here.)
The premise of this question is incorrect: a few deopts don't matter, and don't move the needle regarding performance. Trying to avoid them is an exercise in futility.
The first step when trying to improve performance of something is to profile it. In this case, a profile reveals that the benchmark is spending:
about 46.3% of the time in optimized code (about 4/5 of that for tree creation and 1/5 for tree iteration)
about 0.1% of the time in unoptimized code
about 52.8% of the time in the garbage collector, tracing and freeing all those short-lived objects.
This is as artificial a microbenchmark as they come. 50% GC time never happens in real-world code that does useful things aside from allocating multiple gigabytes of short-lived objects as fast as possible.
In fact, calling them "short-lived objects" is a bit inaccurate in this case. While the vast majority of the individual trees being constructed are indeed short-lived, the code allocates one super-large long-lived tree early on. That fools V8's adaptive mechanisms into assuming that all future TreeNodes will be long-lived too, so it allocates them in "old space" right away -- which would save time if the guess was correct, but ends up wasting time because the TreeNodes that follow are actually short-lived and would be better placed in "new space" (which is optimized for quickly freeing short-lived objects). So just by reshuffling the order of operations, I can get a 3x speedup.
This is a typical example of one of the general problems with microbenchmarks: by doing something extreme and unrealistic, they often create situations that are not at all representative of typical real-world scenarios. If engine developers optimized for such microbenchmarks, engines would perform worse for real-world code. If JavaScript developers try to derive insights from microbenchmarks, they'll write code that performs worse under realistic conditions.
Anyway, if you want to optimize this code, avoid as many of those object allocations as you can.
Concretely:
An artificial microbenchmark like this, by its nature, intentionally does useless work (such as: computing the same value a million times). You said you wanted to optimize it, which means avoiding useless work, but you didn't specify which parts of the useless work you'd like to preserve, if any. So in the absence of a preference, I'll assume that all useless work is useless. So let's optimize!
Looking at the code, it creates perfect binary trees of a given depth and counts their nodes. In other words, it sums up the 1s in these examples:
depth=0:
1
depth=1:
1
/ \
1 1
depth=2:
1
/ \
1 1
/ \ / \
1 1 1 1
and so on. If you think about it for a bit, you'll realize that such a tree of depth N has (2 ** (N+1)) - 1 nodes. So we can replace:
itemCheck(bottomUpTree(depth));
with
(2 ** (depth+1)) - 1
(and analogously for the "stretchDepth" line).
Next, we can take care of the useless repetitions. Since x + x + x + x + ... N times is the same as x*N, we can replace:
let check = 0;
for (let i = 0; i < iterations; i++) {
check += (2 ** (depth + 1)) - 1;
}
with just:
let check = ((2 ** (depth + 1)) - 1) * iterations;
With that we're from 12 seconds down to about 0.1 seconds. Not bad for five minutes of work, eh?
And that remaining time is almost entirely due to the longLivedTree. To apply the same optimizations to the operations creating and iterating that tree, we'd have to move them together, getting rid of its "long-livedness". Would you find that acceptable? You could get the overall time down to less than a millisecond! Would that make the benchmark useless? Not actually any more useless than it was to begin with, just more obviously so.
I'm currently working on creating a chess engine using chess.js, chessboard.js, and the minimax algorithm. I eventually want to implement alpha-beta, but for right now, I just want to get minimax to work. It seems like the computer is thinking, but it usually just does Nc6. If I move the pawn to d4, it usually takes with the knight, but sometimes it just moves the rook back and forth in the spot that was opened up by the knight. If there is nothing for the knight to take, the computer moves the Rook or some other pointless move. My best guess is that all of the moves are returning the same valuation, and so it just makes the first move in the array of possible moves, hence the top left rook being a prime target. I should note that part of my confusion is around the way a recursive function works, and most of the stuff I've found online about recursive functions leaves me more confused than when I started.
I'm using Express.js with the chessboard.js config in public/javascripts as a boardInit.js that's included in the index.ejs folder, and when the user makes a move, a Post request is sent to /moveVsComp. It sends it to the server, where the app.post function for /moveVsComp tells chess.js to make the move that the player made.
After the player move is recorded, the computer calls the computerMoveBlack function.
Function call in the post request:
let compMove = computerMoveBlack(3);
game.load(currentFen)
game.move(compMove)
res.status(200).send({snapback: false, fen: game.fen()})
computerMoveBlack Function:
function computerMoveBlack(depth) {
let bestMove = ['', 105];
for (let move of game.moves()) {
game.move(move)
let value = minimax(move, depth-1, false)
if (value < bestMove[1]) {
bestMove = [move, value]
}
game.undo()
}
console.log(bestMove[0])
return bestMove[0]
}
This function loops through all of the moves, and I was using this because it seemed like this was the best way to keep the best move instead of just returning a valuation of the current position.
Minimax Function:
function minimax(node, depth, maximizingPlayer) {
let value = maximizingPlayer ? -105 : 105
if (depth === 0 || game.game_over()) return getValuation()
if (maximizingPlayer) {
for (let move of game.moves()) {
game.move(move)
value = Math.max(value, minimax(move, depth-1, false))
game.undo()
}
return value
} else {
for (let move of game.moves()) {
game.move(move)
value = Math.min(value, minimax(move, depth-1, true))
game.undo()
}
return value
}
}
getValuation Function:
function getValuation() {
let evalString = game.fen().split(' ')[0];
let score = 0;
score += (evalString.split('r').length -1) * -5 || 0;
score += (evalString.split('b').length -1) * -3 || 0;
score += (evalString.split('n').length -1) * -3 || 0;
score += (evalString.split('q').length -1) * -9 || 0;
score += (evalString.split('p').length -1) * -1 || 0;
score += (evalString.split('R').length -1) * 5 || 0;
score += (evalString.split('N').length -1) * 3 || 0;
score += (evalString.split('B').length -1) * 3 || 0;
score += (evalString.split('Q').length -1) * 9 || 0;
score += (evalString.split('P').length -1) || 0;
return score;
}
I should note that I understand using a FEN in the valuation is very slow for this use case, but I'm not really sure what a better alternative would be.
Just as kind of a recap of the questions, I'm trying to figure out why it just makes the first move in the array every time, what is wrong with the format of my functions, and what a better way to get the valuation of a position is as opposed to string manipulation of the FEN.
I will point out a few suggestions below to help you on the way if you are just getting started. First I just want to say that you are probably right that all moves get the same score and therefore it picks the first possible move. Try to add some Piece Square Tables (PST) to your Evaluation function and see if it puts pieces on appropriate squares.
I would implement a Negamax function instead of Minimax. It is way easier to debug and you won't have to duplicate a lot of code when you later make more optimizations. Negamax is one of the standard chess algorithms.
It seems like you don't do the legal move generation yourself, do you know how the board is represented in the library that you use? Instead of using the FEN for evaluation you want to use the board (or bitboards) to be able to do more advanced evaluation (more on it further down).
The min/max value of -105/105 is not a good way to go. Use -inf and inf instead to not get into troubles later on.
Regarding the evaluation you normally use the board representation to figure out how pieces are placed and how they are working together. Chessprogramming.org is a great resource to read up on different evaluation concepts.
For your simple starting evaluation you could just start with counting up all the material score at the beginning of the game. Then you subtract corresponding piece value when a piece is captured since that is the only case where the score is changing. Now you are recalculating lots of things over and over which will be very slow.
If you want to add PST to the evaluation then you also want to add the piece value change for the moving piece depending on the old and new square. To try and sum up the evaluation:
Sum up all piece values at start-up of a game (with PST scores if you use them) and save it as e.g. whiteScore and blackScore
In your evaluation you subtract the piece value from the opponent if you capture a piece. Otherwise you keep score as is and return it as usual.
If using PST you change the own score based on the new location for the moved piece.
I hope it makes sense, let me know if you need any further help.
So I was solving a problem for class involving binary search and the algorithm I implemented to solve it worked fine but my hunch is that a slight gamble would be more effective given the parameters of the problem
The fictional town of HollyBroke, Fl is made up of a 30 x 30 block grid. The streets are named after the presidents of the United States and the avenues are numbered numerically. The infamous two-word arsonist is holding the town hostage. He selects a house every Saturday for destruction by fire and taunts the police department by challenging them to guess the location for each week’s crime. He will answer up 10 guesses with either a “yes”or a “no” answer during his very brief phone call right before he strikes the match. (He won’t stay on the line so the call can’t be trace.)
The city wants you to develop a program to provide a quick response when this notorious criminal calls.
The answer to that was easy enough to create an algorithm for but I thought a median-1/median+1 gamble would be more effective. My hunch is that more often than not I will arrive at the conclusion with one extra question to go allowing me to either ask a binary search question about the arsonist or if the game allowed it I would show up with police before the end of the call. If I don't outright solve it beforehand I would have a very small space to search after it was completed, like three or four blocks right next to each other,
This is my code for the "gambling" binary search.
`var array = [{"a":30,"b":30,"c":0}]
function findLower(input) {
var half = Math.floor(input/2);
if(0 == input%2)
return (half-1);
else
return (half);
};
function findUpper(input) {
var half = Math.floor(input/2);
if(input%2 == 0)
return (half+1);
else
return (half+1);
}
for (var i = 0; i <= 9; i++){
for (var z = array.length - 1; z >= 0; z--) {
if (array[z].c = i){
if (array[z].a>array[z].b)
array.push({"a":findLower(array[z].a),"b":array[z].b,"c":array[z].c + 1},{"a":findUpper(array[z].a),"b":array[z].b,"c":array[z].c + 1})
else
array.push({"a":array[z].a,"b":findLower(array[z].b),"c":array[z].c + 1},{"a":array[z].a,"b":findUpper(array[z].b),"c":array[z].c + 1})
}
};
}
console.log(array.length);`
Its coming up with an absurd array length given that it should be 2^10 +2^9 + 2^8 ..... = 2047
The program is coming up with an array length of 19683
And some of the arrays should most certainly not be 30*14 at node level 10 I'm sure the algorithm was set up properly. I've walked it through two levels by pen and paper and it seems like it should work properly.
Found it.
if (array[z].c = i){
should be
if (array[z].c == i){
its a conditional statement not declaring them equal
Also I was wrong. You only have about a 40% chance of successfully locating the house in 10 guesses.
I am currently doing program 2 for the startup engineering course offered on coursera
I'm programming using and ubuntu instance using Amazon web services and my programming is constantly hanging. There might be something wrong with my node.js program but I can't seem to locate it.
This program is meant to produce the first 100 Fibonacci numbers separated with commas.
#! /usr/bin/env node
//calculation
var fibonacci = function(n){
if(n < 1){return 0;}
else if(n == 1 || n == 2){return 1;}
else if(n > 2){return fibonacci(n - 1) + fibonacci(n-2);}
};
//put in array
var firstkfib = function(k){
var i;
var arr = [];
for(i = 1; i <= k; i++){
arr.push(fibonacci(i));
}
return arr
};
//print
var format = function(arr){
return arr.join(",");
};
var k = 100;
console.log("firstkfib(" + k +")");
console.log(format(firstkfib(k)));
The only output I get is
ubuntu#ip-172-31-30-245:~$ node fib.js
firstkfib(100)
and then the program hangs
I don't know if you are familiar with Time complexity and algorithmic analysis, but, it turns out that your program has an exponential running time. This basically means that, as the input increases, the time it takes to run your program increases exponentially. (If my explanation is not very clear, check this link)
It turns out that this sort of running time is extremely slow. For example, if it takes 1 ms to run your program for k=1, it would take 2^100 ms to run it for k=100. This turns out to be a ridiculously big number.
In any case, as Zhehao points out, the solution is to save the value of fib(n-1) and fib(n-2) (in an array, for example), and reuse it to compute fib(n). Check out this video lecture from MIT (the first 15 mins) on how to do it.
You may want to try printing out the numbers as they are being computed, instead of printing out the entire list at the end. It's possible that the computation is hanging somewhere along the line.
On another note, this is probably the most inefficient way of computing a list of fibonacci numbers. You compute fibonacci(n) and then fibonacci(n+1) without reusing any of the work from the previous computation. You may want to go back and rethink your method. There's a much faster and simpler iterative method.
writing intense computational code in nodeJS leads to blocking. since Fibonacci is an intense computational code so might end up blocking.
Background
This picture illustrates the problem:
I can control the red circle. The targets are the blue triangles. The black arrows indicate the direction that the targets will move.
I want to collect all targets in the minimum number of steps.
Each turn I must move 1 step either left/right/up or down.
Each turn the targets will also move 1 step according to the directions shown on the board.
Demo
I've put up a playable demo of the problem here on Google appengine.
I would be very interested if anyone can beat the target score as this would show that my current algorithm is suboptimal. (A congratulations message should be printed if you manage this!)
Problem
My current algorithm scales really badly with the number of targets. The time goes up exponentially and for 16 fish it is already several seconds.
I would like to compute the answer for board sizes of 32*32 and with 100 moving targets.
Question
What is an efficient algorithm (ideally in Javascript) for computing the minimum number of steps to collect all targets?
What I've tried
My current approach is based on memoisation but it is very slow and I don't know whether it will always generate the best solution.
I solve the subproblem of "what is the minimum number of steps to collect a given set of targets and end up at a particular target?".
The subproblem is solved recursively by examining each choice for the previous target to have visited.
I assume that it is always optimal to collect the previous subset of targets as quickly as possible and then move from the position you ended up to the current target as quickly as possible (although I don't know whether this is a valid assumption).
This results in n*2^n states to be computed which grows very rapidly.
The current code is shown below:
var DX=[1,0,-1,0];
var DY=[0,1,0,-1];
// Return the location of the given fish at time t
function getPt(fish,t) {
var i;
var x=pts[fish][0];
var y=pts[fish][1];
for(i=0;i<t;i++) {
var b=board[x][y];
x+=DX[b];
y+=DY[b];
}
return [x,y];
}
// Return the number of steps to track down the given fish
// Work by iterating and selecting first time when Manhattan distance matches time
function fastest_route(peng,dest) {
var myx=peng[0];
var myy=peng[1];
var x=dest[0];
var y=dest[1];
var t=0;
while ((Math.abs(x-myx)+Math.abs(y-myy))!=t) {
var b=board[x][y];
x+=DX[b];
y+=DY[b];
t+=1;
}
return t;
}
// Try to compute the shortest path to reach each fish and a certain subset of the others
// key is current fish followed by N bits of bitmask
// value is shortest time
function computeTarget(start_x,start_y) {
cache={};
// Compute the shortest steps to have visited all fish in bitmask
// and with the last visit being to the fish with index equal to last
function go(bitmask,last) {
var i;
var best=100000000;
var key=(last<<num_fish)+bitmask;
if (key in cache) {
return cache[key];
}
// Consider all previous positions
bitmask -= 1<<last;
if (bitmask==0) {
best = fastest_route([start_x,start_y],pts[last]);
} else {
for(i=0;i<pts.length;i++) {
var bit = 1<<i;
if (bitmask&bit) {
var s = go(bitmask,i); // least cost if our previous fish was i
s+=fastest_route(getPt(i,s),getPt(last,s));
if (s<best) best=s;
}
}
}
cache[key]=best;
return best;
}
var t = 100000000;
for(var i=0;i<pts.length;i++) {
t = Math.min(t,go((1<<pts.length)-1,i));
}
return t;
}
What I've considered
Some options that I've wondered about are:
Caching of intermediate results. The distance calculation repeats a lot of simulation and intermediate results could be cached.
However, I don't think this would stop it having exponential complexity.
An A* search algorithm although it is not clear to me what an appropriate admissible heuristic would be and how effective this would be in practice.
Investigating good algorithms for the travelling salesman problem and see if they apply to this problem.
Trying to prove that the problem is NP-hard and hence unreasonable to be seeking an optimal answer for it.
Have you searched the literature? I found these papers which seems to analyse your problem:
"Tracking moving targets and the non- stationary traveling salesman
problem": http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.85.9940
"The moving-target traveling salesman problem": http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.57.6403
UPDATE 1:
The above two papers seems to concentrate on linear movement for the euclidian metric.
Greedy method
One approach suggested in the comments is to go to the closest target first.
I've put up a version of the demo which includes the cost calculated via this greedy method here.
The code is:
function greedyMethod(start_x,start_y) {
var still_to_visit = (1<<pts.length)-1;
var pt=[start_x,start_y];
var s=0;
while (still_to_visit) {
var besti=-1;
var bestc=0;
for(i=0;i<pts.length;i++) {
var bit = 1<<i;
if (still_to_visit&bit) {
c = fastest_route(pt,getPt(i,s));
if (besti<0 || c<bestc) {
besti = i;
bestc = c;
}
}
}
s+=c;
still_to_visit -= 1<<besti;
pt=getPt(besti,s);
}
return s;
}
For 10 targets it is around twice the optimal distance, but sometimes much more (e.g. *4) and occasionally even hits the optimum.
This approach is very efficient so I can afford some cycles to improve the answer.
Next I'm considering using ant colony methods to see if they can explore the solution space effectively.
Ant colony method
An Ant colony method seems to work remarkable well for this problem. The link in this answer now compares the results when using both greedy and ant colony method.
The idea is that ants choose their route probabilistically based on the current level of pheromone. After every 10 trials, we deposit additional pheromone along the shortest trail they found.
function antMethod(start_x,start_y) {
// First establish a baseline based on greedy
var L = greedyMethod(start_x,start_y);
var n = pts.length;
var m = 10; // number of ants
var numrepeats = 100;
var alpha = 0.1;
var q = 0.9;
var t0 = 1/(n*L);
pheromone=new Array(n+1); // entry n used for starting position
for(i=0;i<=n;i++) {
pheromone[i] = new Array(n);
for(j=0;j<n;j++)
pheromone[i][j] = t0;
}
h = new Array(n);
overallBest=10000000;
for(repeat=0;repeat<numrepeats;repeat++) {
for(ant=0;ant<m;ant++) {
route = new Array(n);
var still_to_visit = (1<<n)-1;
var pt=[start_x,start_y];
var s=0;
var last=n;
var step=0;
while (still_to_visit) {
var besti=-1;
var bestc=0;
var totalh=0;
for(i=0;i<pts.length;i++) {
var bit = 1<<i;
if (still_to_visit&bit) {
c = pheromone[last][i]/(1+fastest_route(pt,getPt(i,s)));
h[i] = c;
totalh += h[i];
if (besti<0 || c>bestc) {
besti = i;
bestc = c;
}
}
}
if (Math.random()>0.9) {
thresh = totalh*Math.random();
for(i=0;i<pts.length;i++) {
var bit = 1<<i;
if (still_to_visit&bit) {
thresh -= h[i];
if (thresh<0) {
besti=i;
break;
}
}
}
}
s += fastest_route(pt,getPt(besti,s));
still_to_visit -= 1<<besti;
pt=getPt(besti,s);
route[step]=besti;
step++;
pheromone[last][besti] = (1-alpha) * pheromone[last][besti] + alpha*t0;
last = besti;
}
if (ant==0 || s<bestantscore) {
bestroute=route;
bestantscore = s;
}
}
last = n;
var d = 1/(1+bestantscore);
for(i=0;i<n;i++) {
var besti = bestroute[i];
pheromone[last][besti] = (1-alpha) * pheromone[last][besti] + alpha*d;
last = besti;
}
overallBest = Math.min(overallBest,bestantscore);
}
return overallBest;
}
Results
This ant colony method using 100 repeats of 10 ants is still very fast (37ms for 16 targets compared to 3700ms for the exhaustive search) and seems very accurate.
The table below shows the results for 10 trials using 16 targets:
Greedy Ant Optimal
46 29 29
91 38 37
103 30 30
86 29 29
75 26 22
182 38 36
120 31 28
106 38 30
93 30 30
129 39 38
The ant method seems significantly better than greedy and often very close to optimal.
The problem may be represented in terms of the Generalized Traveling Salesman Problem, and then converted to a conventional Traveling Salesman Problem. This is a well-studied problem. It is possible that the most efficient solutions to the OP's problem are no more efficient than solutions to the TSP, but by no means certain (I am probably failing to take advantage of some aspects of the OP's problem structure that would allow for a quicker solution, such as its cyclical nature). Either way, it is a good starting point.
From C. Noon & J.Bean, An Efficient Transformation of the Generalized Traveling Salesman Problem:
The Generalized Traveling Salesman Problem (GTSP) is a useful model for problems involving decisions of selection and sequence. The asymmetric version of the problem is defined on a directed graph with nodes N, connecting arcs A and a vector of corresponding arc costs c. The nodes are pregrouped into m mutually exclusive and exhaustive nodesets. Connecting arcs are defined only between nodes belonging to different sets, that is, there are no intraset arcs. Each defined arc has a corresponding non-negative cost. The GTSP can be stated as the problem of finding a minimum cost m-arc cycle which includes exactly one node from each nodeset.
For the OP's problem:
Each member of N is a particular fish's location at a particular time. Represent this as (x, y, t), where (x, y) is a grid coordinate, and t is the time at which the fish will be at this coordinate. For the leftmost fish in the OP's example, the first few of these (1-based) are: (3, 9, 1), (4, 9, 2), (5, 9, 3) as the fish moves right.
For any member of N let fish(n_i) return the ID of the fish represented by the node. For any two members of N we can calculate manhattan(n_i, n_j) for the manhattan distance between the two nodes, and time(n_i, n_j) for the time offset between the nodes.
The number of disjoint subsets m is equal to the number of fish. The disjoint subset S_i will consist only of the nodes for which fish(n) == i.
If for two nodes i and j fish(n_i) != fish(n_j) then there is an arc between i and j.
The cost between node i and node j is time(n_i, n_j), or undefined if time(n_i, n_j) < distance(n_i, n_j) (i.e. the location can't be reached before the fish gets there, perhaps because it is backwards in time). Arcs of this latter type can be removed.
An extra node will need to be added representing the location of the player with arcs and costs to all other nodes.
Solving this problem would then result in a single visit to each node subset (i.e. each fish is obtained once) for a path with minimal cost (i.e. minimal time for all fish to be obtained).
The paper goes on to describe how the above formulation may be transformed into a traditional Traveling Salesman Problem and subsequently solved or approximated with existing techniques. I have not read through the details but another paper that does this in a way it proclaims to be efficient is this one.
There are obvious issues with complexity. In particular, the node space is infinite! This can be alleviated by only generating nodes up to a certain time horizon. If t is the number of timesteps to generate nodes for and f is the number of fish then the size of the node space will be t * f. A node at time j will have at most (f - 1) * (t - j) outgoing arcs (as it can't move back in time or to its own subset). The total number of arcs will be in the order of t^2 * f^2 arcs. The arc structure can probably be tidied up, to take advantage of the fact the fish paths are eventually cyclical. The fish will repeat their configuration once every lowest common denominator of their cycle lengths so perhaps this fact can be used.
I don't know enough about the TSP to say whether this is feasible or not, and I don't think it means that the problem posted is necessarily NP-hard... but it is one approach towards finding an optimal or bounded solution.
I think another approch would be:
calculate the path of the targets - predictive.
than use Voronoi diagrams
Quote wikipedia:
In mathematics, a Voronoi diagram is a way of dividing space into a number of regions. A set of points (called seeds, sites, or generators) is specified beforehand and for each seed there will be a corresponding region consisting of all points closer to that seed than to any other.
So, you choose a target, follow it's path for some steps and set a seed point there. Do this with all other targets as well and you get a voroni diagram. Depending in which area you are, you move to the seedpoint of it. Viola, you got the first fish. Now repeat this step until you cought them all.