]> git.sesse.net Git - remoteglot/blob - server/hash-lookup.js
Handle streaming PGNs, like from Lichess (although this might break non-streaming...
[remoteglot] / server / hash-lookup.js
1 var grpc = require('@grpc/grpc-js');
2
3 var PROTO_PATH = __dirname + '/hashprobe.proto';
4 var protoLoader = require('@grpc/proto-loader');
5 var packageDefinition = protoLoader.loadSync(
6     PROTO_PATH,
7     {keepCase: true,
8      longs: String,
9      enums: String,
10      defaults: true,
11      oneofs: true
12     });
13 var hashprobe_proto = grpc.loadPackageDefinition(packageDefinition).hashprobe;
14
15 /*
16  * validate_fen() is taken from chess.js, which has this license:
17  *
18  * Copyright (c) 2017, Jeff Hlywa (jhlywa@gmail.com)
19  * All rights reserved.
20  *
21  * Redistribution and use in source and binary forms, with or without
22  * modification, are permitted provided that the following conditions are met:
23  *
24  * 1. Redistributions of source code must retain the above copyright notice,
25  *    this list of conditions and the following disclaimer.
26  * 2. Redistributions in binary form must reproduce the above copyright notice,
27  *    this list of conditions and the following disclaimer in the documentation
28  *    and/or other materials provided with the distribution.
29  *
30  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
31  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
32  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
33  * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
34  * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
35  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
36  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
37  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
38  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
39  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
40  * POSSIBILITY OF SUCH DAMAGE.
41  *
42  *----------------------------------------------------------------------------*/
43 function validate_fen(fen) {
44   var errors = {
45      0: 'No errors.',
46      1: 'FEN string must contain six space-delimited fields.',
47      2: '6th field (move number) must be a positive integer.',
48      3: '5th field (half move counter) must be a non-negative integer.',
49      4: '4th field (en-passant square) is invalid.',
50      5: '3rd field (castling availability) is invalid.',
51      6: '2nd field (side to move) is invalid.',
52      7: '1st field (piece positions) does not contain 8 \'/\'-delimited rows.',
53      8: '1st field (piece positions) is invalid [consecutive numbers].',
54      9: '1st field (piece positions) is invalid [invalid piece].',
55     10: '1st field (piece positions) is invalid [row too large].',
56     11: 'Illegal en-passant square',
57   };
58
59   /* 1st criterion: 6 space-seperated fields? */
60   var tokens = fen.split(/\s+/);
61   if (tokens.length !== 6) {
62     return {valid: false, error_number: 1, error: errors[1]};
63   }
64
65   /* 2nd criterion: move number field is a integer value > 0? */
66   if (isNaN(tokens[5]) || (parseInt(tokens[5], 10) <= 0)) {
67     return {valid: false, error_number: 2, error: errors[2]};
68   }
69
70   /* 3rd criterion: half move counter is an integer >= 0? */
71   if (isNaN(tokens[4]) || (parseInt(tokens[4], 10) < 0)) {
72     return {valid: false, error_number: 3, error: errors[3]};
73   }
74
75   /* 4th criterion: 4th field is a valid e.p.-string? */
76   if (!/^(-|[abcdefgh][36])$/.test(tokens[3])) {
77     return {valid: false, error_number: 4, error: errors[4]};
78   }
79
80   /* 5th criterion: 3th field is a valid castle-string? */
81   if( !/^[C-HK]?[A-FQ]?[c-hk]?[a-fq]?$/.test(tokens[2]) &&
82       tokens[2] !== '-') {
83     return {valid: false, error_number: 5, error: errors[5]};
84   }
85
86   /* 6th criterion: 2nd field is "w" (white) or "b" (black)? */
87   if (!/^(w|b)$/.test(tokens[1])) {
88     return {valid: false, error_number: 6, error: errors[6]};
89   }
90
91   /* 7th criterion: 1st field contains 8 rows? */
92   var rows = tokens[0].split('/');
93   if (rows.length !== 8) {
94     return {valid: false, error_number: 7, error: errors[7]};
95   }
96
97   /* 8th criterion: every row is valid? */
98   for (var i = 0; i < rows.length; i++) {
99     /* check for right sum of fields AND not two numbers in succession */
100     var sum_fields = 0;
101     var previous_was_number = false;
102
103     for (var k = 0; k < rows[i].length; k++) {
104       if (!isNaN(rows[i][k])) {
105         if (previous_was_number) {
106           return {valid: false, error_number: 8, error: errors[8]};
107         }
108         sum_fields += parseInt(rows[i][k], 10);
109         previous_was_number = true;
110       } else {
111         if (!/^[prnbqkPRNBQK]$/.test(rows[i][k])) {
112           return {valid: false, error_number: 9, error: errors[9]};
113         }
114         sum_fields += 1;
115         previous_was_number = false;
116       }
117     }
118     if (sum_fields !== 8) {
119       return {valid: false, error_number: 10, error: errors[10]};
120     }
121   }
122
123   if ((tokens[3][1] == '3' && tokens[1] == 'w') ||
124       (tokens[3][1] == '6' && tokens[1] == 'b')) {
125         return {valid: false, error_number: 11, error: errors[11]};
126   }
127
128   /* everything's okay! */
129   return {valid: true, error_number: 0, error: errors[0]};
130 }
131
132 var clients = [];
133 var current_servers = [];
134
135 var need_reinit = function(servers) {
136         if (servers.length != current_servers.length) {
137                 return true;
138         }
139         for (var i = 0; i < servers.length; ++i) {
140                 if (servers[i] != current_servers[i]) {
141                         return true;
142                 }
143         }
144         return false;
145 }
146 exports.need_reinit = need_reinit;
147
148 var init = function(servers) {
149         clients = [];
150         for (var i = 0; i < servers.length; ++i) {
151                 clients.push(new hashprobe_proto.HashProbe(servers[i], grpc.credentials.createInsecure()));
152         }
153         current_servers = servers;
154 }
155 exports.init = init;
156
157 var handle_request = function(fen, response) {
158         if (fen === undefined || fen === null || fen === '' || !validate_fen(fen).valid) {
159                 response.writeHead(400, {});
160                 response.end();
161                 return;
162         }
163
164         var rpc_status = {
165                 failed: false,
166                 left: clients.length,
167                 responses: [],
168         }
169         for (var i = 0; i < clients.length; ++i) {
170                 clients[i].probe({fen: fen}, function(err, probe_response) {
171                         if (err) {
172                                 rpc_status.failed = true;
173                         } else {
174                                 rpc_status.responses.push(probe_response);
175                         }
176                         if (--rpc_status.left == 0) {
177                                 // All probes have come back.
178                                 if (rpc_status.failed) {
179                                         response.writeHead(500, {});
180                                         response.end();
181                                 } else {
182                                         handle_response(fen, response, rpc_status.responses);
183                                 }
184                         }
185                 });
186         }
187 }
188 exports.handle_request = handle_request;
189
190 var handle_response = function(fen, response, probe_responses) {
191         var probe_response = reconcile_responses(probe_responses);
192         var lines = {};
193
194         var root = translate_line(fen, probe_response['root']);
195         for (var i = 0; i < probe_response['line'].length; ++i) {
196                 var line = probe_response['line'][i];
197                 var pretty_move = line['move']['pretty'];
198                 lines[pretty_move] = translate_line(fen, line);
199         }
200
201         var text = JSON.stringify({
202                 root: root,
203                 lines: lines
204         });
205         var headers = {
206                 'Content-Type': 'text/json; charset=utf-8'
207                 //'Content-Length': text.length
208         };
209         response.writeHead(200, headers);
210         response.write(text);
211         response.end();
212 }
213
214 var reconcile_responses = function(probe_responses) {
215         var probe_response = {};
216
217         // Select the root that has searched the deepest, plain and simple.
218         probe_response['root'] = probe_responses[0]['root'];
219         for (var i = 1; i < probe_responses.length; ++i) {
220                 var root = probe_responses[i]['root'];
221                 if (root['depth'] > probe_response['root']['depth']) {
222                         probe_response['root'] = root;
223                 }
224         }
225
226         // Do the same ting for each move, combining on move.
227         var moves = {};
228         for (var i = 0; i < probe_responses.length; ++i) {
229                 for (var j = 0; j < probe_responses[i]['line'].length; ++j) {
230                         var line = probe_responses[i]['line'][j];
231                         var pretty_move = line['move']['pretty'];
232
233                         if (!moves[pretty_move]) {
234                                 moves[pretty_move] = line;
235                         } else {
236                                 moves[pretty_move] = reconcile_moves(line, moves[pretty_move]);
237                         }
238                 }
239         }
240         probe_response['line'] = [];
241         for (var move in moves) {
242                 probe_response['line'].push(moves[move]);
243         }
244         return probe_response;
245 }
246
247 var reconcile_moves = function(a, b) {
248         // Prefer exact bounds, unless the depth is just so much higher.
249         if (a['bound'] === 'BOUND_EXACT' &&
250             b['bound'] !== 'BOUND_EXACT' &&
251             a['depth'] + 10 >= b['depth']) {
252                 return a;
253         }
254         if (b['bound'] === 'BOUND_EXACT' &&
255             a['bound'] !== 'BOUND_EXACT' &&
256             b['depth'] + 10 >= a['depth']) {
257                 return b;
258         }
259
260         if (a['depth'] > b['depth']) {
261                 return a;
262         } else {
263                 return b;
264         }
265 }       
266
267 var translate_line = function(fen, line) {
268         var r = {};
269
270         if (line['move'] && line['move']['pretty']) {
271                 r['move'] = line['move']['pretty']
272         } else {
273                 r['move'] = '';
274         }
275         if (!line['found']) {
276                 r['pv'] = [];
277                 return r;
278         }
279         r['depth'] = line['depth'];
280
281         // Convert the PV.
282         var pv = [];
283         if (r['move']) {
284                 pv.push(r['move']);
285         }
286         for (var j = 0; j < line['pv'].length; ++j) {
287                 var move = line['pv'][j];
288                 pv.push(move['pretty']);
289         }
290         r['pv'] = pv;
291
292         // Convert the score. Use the static eval if no search.
293         var value = line['value'] || line['eval'];
294         var score = null;
295         if (value['score_type'] === 'SCORE_CP') {
296                 score = ['cp', value['score_cp']];
297         } else if (value['score_type'] === 'SCORE_MATE') {
298                 score = ['m', value['score_mate']];
299         }
300         if (score) {
301                 if (line['bound'] === 'BOUND_UPPER') {
302                         score.push('≤');
303                 } else if (line['bound'] === 'BOUND_LOWER') {
304                         score.push('≥');
305                 }
306         }
307
308         r['score'] = score;
309
310         return r;
311 }