]> git.sesse.net Git - remoteglot/blob - www/js/remoteglot.js
bff7a846bb5cbb724ab29d35857e7079c1e566c4
[remoteglot] / www / js / remoteglot.js
1 var board = [];
2 var arrows = [];
3 var arrow_targets = [];
4 var occupied_by_arrows = [];
5 var ims = 0;
6 var highlight_from = undefined;
7 var highlight_to = undefined;
8 var unique = Math.random();
9
10 var request_update = function(board) {
11         $.ajax({
12                 url: "http://analysis.sesse.net/analysis.pl?ims=" + ims + "&unique=" + unique
13                 //url: "http://analysis.sesse.net:5000/analysis.pl?ims=" + ims + "&unique=" + unique
14         }).done(function(data, textstatus, xhr) {
15                 ims = xhr.getResponseHeader('X-Remoteglot-Last-Modified');
16                 var num_viewers = xhr.getResponseHeader('X-Remoteglot-Num-Viewers');
17                 update_board(board, data, num_viewers);
18         });
19 }
20
21 var clear_arrows = function() {
22         for (var i = 0; i < arrows.length; ++i) {
23                 jsPlumb.detach(arrows[i].connection1);
24                 jsPlumb.detach(arrows[i].connection2);
25         }
26         arrows = [];
27
28         for (var i = 0; i < arrow_targets.length; ++i) {
29                 document.body.removeChild(arrow_targets[i]);
30         }
31         arrow_targets = [];
32         
33         occupied_by_arrows = [];        
34         for (var y = 0; y < 8; ++y) {
35                 occupied_by_arrows.push([false, false, false, false, false, false, false, false]);
36         }
37 }
38
39 var redraw_arrows = function() {
40         for (var i = 0; i < arrows.length; ++i) {
41                 position_arrow(arrows[i]);
42         }
43 }
44
45 var sign = function(x) {
46         if (x > 0) {
47                 return 1;
48         } else if (x < 0) {
49                 return -1;
50         } else {
51                 return 0;
52         }
53 }
54
55 // See if drawing this arrow on the board would cause unduly amount of confusion.
56 var interfering_arrow = function(from, to) {
57         var from_col = from.charCodeAt(0) - "a1".charCodeAt(0);
58         var from_row = from.charCodeAt(1) - "a1".charCodeAt(1);
59         var to_col   = to.charCodeAt(0) - "a1".charCodeAt(0);
60         var to_row   = to.charCodeAt(1) - "a1".charCodeAt(1);
61
62         occupied_by_arrows[from_row][from_col] = true;
63
64         // Knight move: Just check that we haven't been at the destination before.
65         if ((Math.abs(to_col - from_col) == 2 && Math.abs(to_row - from_row) == 1) ||
66             (Math.abs(to_col - from_col) == 1 && Math.abs(to_row - from_row) == 2)) {
67                 return occupied_by_arrows[to_row][to_col];
68         }
69
70         // Sliding piece: Check if anything except the from-square is seen before.
71         var dx = sign(to_col - from_col);
72         var dy = sign(to_row - from_row);
73         var x = from_col;
74         var y = from_row;
75         do {
76                 x += dx;
77                 y += dy;
78                 if (occupied_by_arrows[y][x]) {
79                         return true;
80                 }
81                 occupied_by_arrows[y][x] = true;
82         } while (x != to_col || y != to_row);
83
84         return false;
85 }
86
87 var add_target = function() {
88         var elem = document.createElement("div");
89         $(elem).addClass("window");
90         elem.id = "target" + arrow_targets.length;
91         document.body.appendChild(elem);        
92         arrow_targets.push(elem);
93         return elem.id;
94 }
95         
96 var position_arrow = function(arrow) {
97         var zoom_factor = $("#board").width() / 400.0;
98         var line_width = arrow.line_width * zoom_factor;
99         var arrow_size = arrow.arrow_size * zoom_factor;
100
101         var square_width = $(".square-a8").width();
102         var from_y = (7 - arrow.from_row + 0.5)*square_width;
103         var to_y = (7 - arrow.to_row + 0.5)*square_width;
104         var from_x = (arrow.from_col + 0.5)*square_width;
105         var to_x = (arrow.to_col + 0.5)*square_width;
106
107         var dx = to_x - from_x;
108         var dy = to_y - from_y;
109         var len = Math.sqrt(dx * dx + dy * dy);
110         dx /= len;
111         dy /= len;
112         var pos = $(".square-a8").position();
113         $("#" + arrow.s1).css({ top: pos.top + from_y + (0.5 * arrow_size) * dy, left: pos.left + from_x + (0.5 * arrow_size) * dx });
114         $("#" + arrow.d1).css({ top: pos.top + to_y - (0.5 * arrow_size) * dy, left: pos.left + to_x - (0.5 * arrow_size) * dx });
115         $("#" + arrow.s1v).css({ top: pos.top + from_y - 0 * dy, left: pos.left + from_x - 0 * dx });
116         $("#" + arrow.d1v).css({ top: pos.top + to_y + 0 * dy, left: pos.left + to_x + 0 * dx });
117
118         if (arrow.connection1) {
119                 jsPlumb.detach(arrow.connection1);
120         }
121         if (arrow.connection2) {
122                 jsPlumb.detach(arrow.connection2);
123         }
124         arrow.connection1 = jsPlumb.connect({
125                 source: arrow.s1,
126                 target: arrow.d1,
127                 connector:["Straight"],
128                 cssClass:"c1",
129                 endpoint:"Blank",
130                 endpointClass:"c1Endpoint",                                                                                                        
131                 anchor:"Continuous",
132                 paintStyle:{ 
133                         lineWidth:line_width,
134                         strokeStyle:arrow.fg_color,
135                         outlineWidth:1,
136                         outlineColor:"#666",
137                         opacity:"60%"
138                 }
139         });
140         arrow.connection2 = jsPlumb.connect({
141                 source: arrow.s1v,
142                 target: arrow.d1v,
143                 connector:["Straight"],
144                 cssClass:"vir",
145                 endpoint:"Blank",
146                 endpointClass:"c1Endpoint",                                                                                                        
147                 anchor:"Continuous",
148                 paintStyle:{ 
149                         lineWidth:0,
150                         strokeStyle:arrow.fg_color,
151                         outlineWidth:0,
152                         outlineColor:"#666",
153                 },
154                 overlays : [
155                         ["Arrow", {
156                                 cssClass:"l1arrow",
157                                 location:1.0,
158                                 width: arrow_size,
159                                 length: arrow_size,
160                                 paintStyle: { 
161                                         lineWidth:line_width,
162                                         strokeStyle:"#000",
163                                 },
164                         }]
165                 ]
166         });
167 }
168
169 var create_arrow = function(from_square, to_square, fg_color, line_width, arrow_size) {
170         var from_col = from_square.charCodeAt(0) - "a1".charCodeAt(0);
171         var from_row = from_square.charCodeAt(1) - "a1".charCodeAt(1);
172         var to_col   = to_square.charCodeAt(0) - "a1".charCodeAt(0);
173         var to_row   = to_square.charCodeAt(1) - "a1".charCodeAt(1);
174
175         // Create arrow.
176         var arrow = {
177                 s1: add_target(),
178                 d1: add_target(),
179                 s1v: add_target(),
180                 d1v: add_target(),
181                 from_col: from_col,
182                 from_row: from_row,
183                 to_col: to_col,
184                 to_row: to_row,
185                 line_width: line_width,
186                 arrow_size: arrow_size, 
187                 fg_color: fg_color
188         };
189
190         position_arrow(arrow);
191         arrows.push(arrow);
192 }
193
194 // Fake multi-PV using the refutation lines. Find all “relevant” moves,
195 // sorted by quality, descending.
196 var find_nonstupid_moves = function(data, margin) {
197         // First of all, if there are any moves that are more than 0.5 ahead of
198         // the primary move, the refutation lines are probably bunk, so just
199         // kill them all. 
200         var best_score = undefined;
201         var pv_score = undefined;
202         for (var move in data.refutation_lines) {
203                 var score = parseInt(data.refutation_lines[move].score_sort_key);
204                 if (move == data.pv_uci[0]) {
205                         pv_score = score;
206                 }
207                 if (best_score === undefined || score > best_score) {
208                         best_score = score;
209                 }
210                 if (!(data.refutation_lines[move].depth >= 8)) {
211                         return [];
212                 }
213         }
214
215         if (best_score - pv_score > 50) {
216                 return [];
217         }
218
219         // Now find all moves that are within “margin” of the best score.
220         // The PV move will always be first.
221         var moves = [];
222         for (var move in data.refutation_lines) {
223                 var score = parseInt(data.refutation_lines[move].score_sort_key);
224                 if (move != data.pv_uci[0] && best_score - score <= margin) {
225                         moves.push(move);
226                 }
227         }
228         moves = moves.sort(function(a, b) { return parseInt(data.refutation_lines[b].score_sort_key) - parseInt(data.refutation_lines[a].score_sort_key); });
229         moves.unshift(data.pv_uci[0]);
230
231         return moves;
232 }
233
234 var thousands = function(x) {
235         return String(x).split('').reverse().join('').replace(/(\d{3}\B)/g, '$1,').split('').reverse().join('');
236 }
237
238 var print_pv = function(pretty_pv, move_num, toplay, limit) {
239         var pv = '';
240         var i = 0;
241         if (toplay == 'B') {
242                 pv = move_num + '. … ' + pretty_pv[0];
243                 toplay = 'W';
244                 ++i;    
245         }
246         ++move_num;
247         for ( ; i < pretty_pv.length; ++i) {
248                 if (toplay == 'W') {
249                         if (i > limit) {
250                                 return pv + ' (…)';
251                         }
252                         if (pv != '') {
253                                 pv += ' ';
254                         }
255                         pv += move_num + '. ' + pretty_pv[i];
256                         ++move_num;
257                         toplay = 'B';
258                 } else {
259                         pv += ' ' + pretty_pv[i];
260                         toplay = 'W';
261                 }
262         }
263         return pv;
264 }
265
266 var compare_by_sort_key = function(data, a, b) {
267         var ska = data.refutation_lines[a].sort_key;
268         var skb = data.refutation_lines[b].sort_key;
269         if (ska < skb) return -1;
270         if (ska > skb) return 1;
271         return 0;
272 };
273         
274 var update_highlight = function()  {
275         $("#board").find('.square-55d63').removeClass('nonuglyhighlight');
276         if (highlight_from !== undefined && highlight_to !== undefined) {
277                 $("#board").find('.square-' + highlight_from).addClass('nonuglyhighlight');
278                 $("#board").find('.square-' + highlight_to).addClass('nonuglyhighlight');
279         }
280 }
281
282 var update_board = function(board, data, num_viewers) {
283         // The headline.
284         var headline = 'Analysis';
285         if (data.position.last_move !== 'none') {
286                 headline += ' after ' + data.position.move_num + '. ';
287                 if (data.position.toplay == 'W') {
288                         headline += '… ';
289                 }
290                 headline += data.position.last_move;
291         }
292
293         $("#headline").text(headline);
294
295         if (num_viewers === null) {
296                 $("#numviewers").text("");
297         } else if (num_viewers == 1) {
298                 $("#numviewers").text("You are the only current viewer");
299         } else {
300                 $("#numviewers").text(num_viewers + " current viewers");
301         }
302
303         // The score.
304         if (data.score !== null) {
305                 $("#score").text(data.score);
306         }
307
308         // The search stats.
309         if (data.nodes && data.nps && data.depth) {
310                 var stats = thousands(data.nodes) + ' nodes, ' + thousands(data.nps) + ' nodes/sec, depth ' + data.depth + ' ply';
311                 if (data.seldepth) {
312                         stats += ' (' + data.seldepth + ' selective)';
313                 }
314                 if (data.tbhits && data.tbhits > 0) {
315                         if (data.tbhits == 1) {
316                                 stats += ', one Nalimov hit';
317                         } else {
318                                 stats += ', ' + data.tbhits + ' Nalimov hits';
319                         }
320                 }
321                 
322
323                 $("#searchstats").text(stats);
324         }
325
326         // Update the board itself.
327         board.position(data.position.fen);
328
329         if (data.position.last_move_uci) {
330                 highlight_from = data.position.last_move_uci.substr(0, 2);
331                 highlight_to = data.position.last_move_uci.substr(2, 4);
332         } else {
333                 highlight_from = highlight_to = undefined;
334         }
335         update_highlight();
336
337         // Print the PV.
338         var pv = print_pv(data.pv_pretty, data.position.move_num, data.position.toplay);
339         $("#pv").text(pv);
340
341         // Update the PV arrow.
342         clear_arrows();
343         if (data.pv_uci.length >= 1) {
344                 // draw a continuation arrow as long as it's the same piece
345                 for (var i = 0; i < data.pv_uci.length; i += 2) {
346                         var from = data.pv_uci[i].substr(0, 2);
347                         var to = data.pv_uci[i].substr(2,4);
348                         if ((i >= 2 && from != data.pv_uci[i - 2].substr(2, 4)) ||
349                              interfering_arrow(from, to)) {
350                                 break;
351                         }
352                         create_arrow(from, to, '#f66', 6, 20);
353                 }
354
355                 var alt_moves = find_nonstupid_moves(data, 30);
356                 for (var i = 1; i < alt_moves.length && i < 3; ++i) {
357                         create_arrow(alt_moves[i].substr(0, 2),
358                                      alt_moves[i].substr(2, 4), '#f66', 1, 10);
359                 }
360         }
361
362         // See if all semi-reasonable moves have only one possible response.
363         if (data.pv_uci.length >= 2) {
364                 var nonstupid_moves = find_nonstupid_moves(data, 300);
365                 var response = data.pv_uci[1];
366                 for (var i = 0; i < nonstupid_moves.length; ++i) {
367                         if (nonstupid_moves[i] == data.pv_uci[0]) {
368                                 // ignore the PV move for refutation lines.
369                                 continue;
370                         }
371                         if (!data.refutation_lines ||
372                             !data.refutation_lines[nonstupid_moves[i]] ||
373                             !data.refutation_lines[nonstupid_moves[i]].pv_uci ||
374                             data.refutation_lines[nonstupid_moves[i]].pv_uci.length < 1) {
375                                 // Incomplete PV, abort.
376                                 response = undefined;
377                                 break;
378                         }
379                         var this_response = data.refutation_lines[nonstupid_moves[i]].pv_uci[1];
380                         if (response !== this_response) {
381                                 // Different response depending on lines, abort.
382                                 response = undefined;
383                                 break;
384                         }
385                 }
386
387                 if (nonstupid_moves.length > 0 && response !== undefined) {
388                         create_arrow(response.substr(0, 2),
389                                      response.substr(2, 4), '#66f', 6, 20);
390                 }
391         }
392
393         // Show the refutation lines.
394         var tbl = $("#refutationlines");
395         tbl.empty();
396
397         moves = [];
398         for (var move in data.refutation_lines) {
399                 moves.push(move);
400         }
401         moves = moves.sort(function(a, b) { return compare_by_sort_key(data, a, b) });
402         for (var i = 0; i < moves.length; ++i) {
403                 var line = data.refutation_lines[moves[i]];
404
405                 var tr = document.createElement("tr");
406
407                 var move_td = document.createElement("td");
408                 tr.appendChild(move_td);
409                 $(move_td).addClass("move");
410                 $(move_td).text(line.pretty_move);
411
412                 var score_td = document.createElement("td");
413                 tr.appendChild(score_td);
414                 $(score_td).addClass("score");
415                 $(score_td).text(line.pretty_score);
416
417                 var depth_td = document.createElement("td");
418                 tr.appendChild(depth_td);
419                 $(depth_td).addClass("depth");
420                 $(depth_td).text("d" + line.depth);
421
422                 var pv_td = document.createElement("td");
423                 tr.appendChild(pv_td);
424                 $(pv_td).addClass("pv");
425                 $(pv_td).text(print_pv(line.pv_pretty, data.position.move_num, data.position.toplay, 10));
426
427                 tbl.append(tr);
428         }
429
430         // Next update.
431         setTimeout(function() { request_update(board); }, 100);
432 }
433
434 var init = function() {
435         // Create board.
436         board = new ChessBoard('board', 'start');
437
438         request_update(board);
439         $(window).resize(function() {
440                 board.resize();
441                 update_highlight();
442                 redraw_arrows();
443         });
444 };
445 $(document).ready(init);