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