Move the CSS into a separate file.
[remoteglot] / www / index.html
1 <!doctype html>
2 <html>
3 <head>
4   <meta charset="utf-8" />
5   <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
6   <title>analysis.sesse.net</title>
7
8   <link rel="stylesheet" href="css/chessboard-0.3.0.min.css" />
9   <link rel="stylesheet" href="css/remoteglot.css" />
10 </head>
11 <body>
12 <h1 id="headline">Analysis</h1>
13 <table>
14 <tr>
15 <td>
16 <div id="board" style="width: 400px"></div>
17 </td>
18 <td>
19   <p id="score">Score:</p>
20   <p><strong>PV:</strong> <span id="pv"></span></p>
21   <p id="searchstats"></p>
22   <h3 style="margin-top: 1em; margin-bottom: 0;">Shallow search of all legal moves (multi-PV)</h3>
23   <table id="refutationlines">
24   </table>
25 </td>
26 </tr>
27 </table>
28 <h2>Symbol explanation</h2>
29 <ul>
30   <li><strong>Score:</strong> 1.00 is the value of one pawn (in the opening). Positive values are better for white.</li>
31   <li><strong>PV:</strong> Principal Variation, the series of moves the engine thinks is the best.</li>
32   <li><strong>Thick red line:</strong> Marks the best move (in the view of the engine). Multiple chained arrows
33     means that the PV starts with multiple successive moves with the same piece, ie., the engine thinks
34     that the piece will execute a maneuver.</li>
35   <li><strong>Thin red lines:</strong> Other good moves, maximum two. Note that even though these are also
36     quality checked, these are less thoroughly analyzed by the engine,
37     and should be taken with a grain of salt.</li>
38   <li><strong>Thick blue line:</strong> Marks the best <em>response</em> move. Note that this is only rarely shown,
39     since usually, the best response move depends on what the first move is. A typical case is when the current move
40     is forced or nearly so.</li>
41 </ul>
42 <p id="credits"><a href="http://git.sesse.net/?p=remoteglot;a=summary">remoteglot</a>
43   &copy; 2007-2013 <a href="http://www.sesse.net/">Steinar H. Gunderson</a>.
44   Chess analysis by <a href="http://stockfishchess.org/">Stockfish</a> (main analysis: 12x2.3GHz Sandy Bridge,
45   multi-PV search: 8x2.27GHz Nehalem).
46   Moves provided by <a href="http://www.freechess.org/">FICS</a>.
47   Hosting and main analysis hardware by <a href="http://www.samfundet.no/">Studentersamfundet i Trondhjem</a>.
48   JavaScript chessboard powered by <a href="http://chessboardjs.com/">chessboard.js</a>.
49   Arrows by <a href="http://jsplumbtoolkit.com">jsPlumb</a>&mdash;who would think you could
50   draw such beautiful arrows in just 161 kB of JavaScript on top of the
51   323 kB needed for jQuery and jQuery UI? How far technology has come. If you want something
52   more retro, the <a href="/text.pl">text interface</a> is still available.</p>
53
54 <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.9.0/jquery.min.js"></script>
55 <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jqueryui/1.9.2/jquery-ui.min.js"></script>
56 <script type="text/javascript" src="js/jquery.jsPlumb-1.5.3-min.js"></script>
57 <script type="text/javascript" src="js/chessboard-0.3.0.min.js"></script>
58 <script>
59 var arrows = [];
60 var arrow_targets = [];
61 var occupied_by_arrows = [];
62
63 var request_update = function(board, first) {
64         $.ajax({
65                 //url: "http://analysis.sesse.net/analysis.pl?first=" + first
66                 url: "http://analysis.sesse.net:5000/analysis.pl?first=" + first
67         }).done(function(data) {
68                 update_board(board, data);
69         });
70 }
71
72 var clear_arrows = function() {
73         for (var i = 0; i < arrows.length; ++i) {
74                 jsPlumb.detach(arrows[i]);
75         }
76         arrows = [];
77
78         for (var i = 0; i < arrow_targets.length; ++i) {
79                 document.body.removeChild(arrow_targets[i]);
80         }
81         arrow_targets = [];
82         
83         occupied_by_arrows = [];        
84         for (var y = 0; y < 8; ++y) {
85                 occupied_by_arrows.push([false, false, false, false, false, false, false, false]);
86         }
87 }
88
89 var sign = function(x) {
90         if (x > 0) {
91                 return 1;
92         } else if (x < 0) {
93                 return -1;
94         } else {
95                 return 0;
96         }
97 }
98
99 // See if drawing this arrow on the board would cause unduly amount of confusion.
100 var interfering_arrow = function(from, to) {
101         var from_col = from.charCodeAt(0) - "a1".charCodeAt(0);
102         var from_row = from.charCodeAt(1) - "a1".charCodeAt(1);
103         var to_col   = to.charCodeAt(0) - "a1".charCodeAt(0);
104         var to_row   = to.charCodeAt(1) - "a1".charCodeAt(1);
105
106         occupied_by_arrows[from_row][from_col] = true;
107
108         // Knight move: Just check that we haven't been at the destination before.
109         if ((Math.abs(to_col - from_col) == 2 && Math.abs(to_row - from_row) == 1) ||
110             (Math.abs(to_col - from_col) == 1 && Math.abs(to_row - from_row) == 2)) {
111                 return occupied_by_arrows[to_row][to_col];
112         }
113
114         // Sliding piece: Check if anything except the from-square is seen before.
115         var dx = sign(to_col - from_col);
116         var dy = sign(to_row - from_row);
117         var x = from_col;
118         var y = from_row;
119         do {
120                 x += dx;
121                 y += dy;
122                 if (occupied_by_arrows[y][x]) {
123                         return true;
124                 }
125                 occupied_by_arrows[y][x] = true;
126         } while (x != to_col || y != to_row);
127
128         return false;
129 }
130
131 var add_target = function() {
132         var elem = document.createElement("div");
133         $(elem).addClass("window");
134         elem.id = "target" + arrow_targets.length;
135         document.body.appendChild(elem);        
136         arrow_targets.push(elem);
137         return elem.id;
138 }
139
140 var create_arrow = function(from_square, to_square, fg_color, line_width, arrow_size) {
141         var from_col = from_square.charCodeAt(0) - "a1".charCodeAt(0);
142         var from_row = from_square.charCodeAt(1) - "a1".charCodeAt(1);
143         var to_col   = to_square.charCodeAt(0) - "a1".charCodeAt(0);
144         var to_row   = to_square.charCodeAt(1) - "a1".charCodeAt(1);
145
146         var from_y = (7 - from_row)*49 + 25;
147         var to_y = (7 - to_row)*49 + 25;
148         var from_x = from_col*49 + 25;
149         var to_x = to_col*49 + 25;
150
151         var dx = to_x - from_x;
152         var dy = to_y - from_y;
153         var len = Math.sqrt(dx * dx + dy * dy);
154         dx /= len;
155         dy /= len;
156
157         // Create arrow.
158         var s1 = add_target();
159         var d1 = add_target();
160         var s1v = add_target();
161         var d1v = add_target();
162         var pos = $("#board").position();
163         $("#" + s1).css({ top: pos.top + from_y + (0.5 * arrow_size) * dy, left: pos.left + from_x + (0.5 * arrow_size) * dx });
164         $("#" + d1).css({ top: pos.top + to_y - (0.5 * arrow_size) * dy, left: pos.left + to_x - (0.5 * arrow_size) * dx });
165         $("#" + s1v).css({ top: pos.top + from_y - 0 * dy, left: pos.left + from_x - 0 * dx });
166         $("#" + d1v).css({ top: pos.top + to_y + 0 * dy, left: pos.left + to_x + 0 * dx });
167         var connection1 = jsPlumb.connect({
168                 source: s1,
169                 target: d1,
170                 connector:["Straight"],
171                 cssClass:"c1",
172                 endpoint:"Blank",
173                 endpointClass:"c1Endpoint",                                                                                                        
174                 anchor:"Continuous",
175                 paintStyle:{ 
176                         lineWidth:line_width,
177                         strokeStyle:fg_color,
178                         outlineWidth:1,
179                         outlineColor:"#666",
180                         opacity:"60%"
181                 }
182         });            
183         var connection2 = jsPlumb.connect({
184                 source: s1v,
185                 target: d1v,
186                 connector:["Straight"],
187                 cssClass:"vir",
188                 endpoint:"Blank",
189                 endpointClass:"c1Endpoint",                                                                                                        
190                 anchor:"Continuous",
191                 paintStyle:{ 
192                         lineWidth:0,
193                         strokeStyle:fg_color,
194                         outlineWidth:0,
195                         outlineColor:"#666",
196                 },
197                 overlays : [
198                         ["Arrow", {
199                                 cssClass:"l1arrow",
200                                 location:1.0,
201                                 width: arrow_size, length: arrow_size,
202                                 paintStyle:{ 
203                                         lineWidth:line_width,
204                                         strokeStyle:"#000",
205                                 },
206                         }]
207                 ]
208         });
209         arrows.push(connection1);
210         arrows.push(connection2);
211 }
212
213 // Fake multi-PV using the refutation lines. Find all “relevant” moves,
214 // sorted by quality, descending.
215 var find_nonstupid_moves = function(data, margin) {
216         // First of all, if there are any moves that are more than 0.5 ahead of
217         // the primary move, the refutation lines are probably bunk, so just
218         // kill them all. 
219         var best_score = undefined;
220         var pv_score = undefined;
221         for (var move in data.refutation_lines) {
222                 var score = data.refutation_lines[move].score_sort_key;
223                 if (move == data.pv_uci[0]) {
224                         pv_score = score;
225                 }
226                 if (best_score === undefined || score > best_score) {
227                         best_score = score;
228                 }
229                 if (!(data.refutation_lines[move].depth >= 8)) {
230                         return [];
231                 }
232         }
233
234         if (best_score - pv_score > 50) {
235                 return [];
236         }
237
238         // Now find all moves that are within “margin” of the best score.
239         // The PV move will always be first.
240         var moves = [];
241         for (var move in data.refutation_lines) {
242                 var score = data.refutation_lines[move].score_sort_key;
243                 if (move != data.pv_uci[0] && best_score - score <= margin) {
244                         moves.push(move);
245                 }
246         }
247         moves = moves.sort(function(a, b) { return data.refutation_lines[b].score_sort_key - data.refutation_lines[a].score_sort_key; });
248         moves.unshift(data.pv_uci[0]);
249
250         return moves;
251 }
252
253 var thousands = function(x) {
254         return String(x).split('').reverse().join('').replace(/(\d{3}\B)/g, '$1,').split('').reverse().join('');
255 }
256
257 var print_pv = function(pretty_pv, move_num, toplay, limit) {
258         var pv = '';
259         var i = 0;
260         if (toplay == 'B') {
261                 pv = move_num + '. … ' + pretty_pv[0];
262                 toplay = 'W';
263                 ++i;    
264         }
265         ++move_num;
266         for ( ; i < pretty_pv.length; ++i) {
267                 if (toplay == 'W') {
268                         if (i > limit) {
269                                 return pv + ' (…)';
270                         }
271                         if (pv != '') {
272                                 pv += ' ';
273                         }
274                         pv += move_num + '. ' + pretty_pv[i];
275                         ++move_num;
276                         toplay = 'B';
277                 } else {
278                         pv += ' ' + pretty_pv[i];
279                         toplay = 'W';
280                 }
281         }
282         return pv;
283 }
284
285 var compare_by_sort_key = function(data, a, b) {
286         var ska = data.refutation_lines[a].sort_key;
287         var skb = data.refutation_lines[b].sort_key;
288         if (ska < skb) return -1;
289         if (ska > skb) return 1;
290         return 0;
291 };
292
293 var update_board = function(board, data) {
294         // The headline.
295         var headline = 'Analysis';
296         if (data.position.last_move !== 'none') {
297                 headline += ' after ' + data.position.move_num + '. ';
298                 if (data.position.toplay == 'W') {
299                         headline += '… ';
300                 }
301                 headline += data.position.last_move;
302                 if (data.id.name) {
303                         headline += ', ';
304                 }
305         }
306
307         if (data.id.name) {
308                 headline += ' by ' + data.id.name;  // + ':';
309         } else {
310                 //headline += ':';
311         }
312         $("#headline").text(headline);
313
314         // The score.
315         if (data.score !== null) {
316                 $("#score").text(data.score);
317         }
318
319         // The search stats.
320         if (data.nodes && data.nps && data.depth) {
321                 var stats = thousands(data.nodes) + ' nodes, ' + thousands(data.nps) + ' nodes/sec, depth ' + data.depth + ' ply';
322                 if (data.seldepth) {
323                         stats += ' (' + data.seldepth + ' selective)';
324                 }
325                 if (data.tbhits && data.tbhits > 0) {
326                         if (data.tbhits == 1) {
327                                 stats += ', one Nalimov hit';
328                         } else {
329                                 stats += ', ' + data.tbhits + ' Nalimov hits';
330                         }
331                 }
332                 
333
334                 $("#searchstats").text(stats);
335         }
336
337         // Update the board itself.
338         board.position(data.position.fen);
339
340         $("#board").find('.square-55d63').removeClass('nonuglyhighlight');
341         if (data.position.last_move_uci) {
342                 var from = data.position.last_move_uci.substr(0, 2);
343                 var to = data.position.last_move_uci.substr(2, 4);
344                 $("#board").find('.square-' + from).addClass('nonuglyhighlight');
345                 $("#board").find('.square-' + to).addClass('nonuglyhighlight');
346         }
347
348         // Print the PV.
349         var pv = print_pv(data.pv_pretty, data.position.move_num, data.position.toplay);
350         $("#pv").text(pv);
351
352         // Update the PV arrow.
353         clear_arrows();
354         if (data.pv_uci.length >= 1) {
355                 // draw a continuation arrow as long as it's the same piece
356                 for (var i = 0; i < data.pv_uci.length; i += 2) {
357                         var from = data.pv_uci[i].substr(0, 2);
358                         var to = data.pv_uci[i].substr(2,4);
359                         if ((i >= 2 && from != data.pv_uci[i - 2].substr(2, 4)) ||
360                              interfering_arrow(from, to)) {
361                                 break;
362                         }
363                         create_arrow(from, to, '#f66', 6, 20);
364                 }
365
366                 var alt_moves = find_nonstupid_moves(data, 30);
367                 for (var i = 1; i < alt_moves.length && i < 3; ++i) {
368                         create_arrow(alt_moves[i].substr(0, 2),
369                                      alt_moves[i].substr(2, 4), '#f66', 1, 10);
370                 }
371         }
372
373         // See if all semi-reasonable moves have only one possible response.
374         if (data.pv_uci.length >= 2) {
375                 var nonstupid_moves = find_nonstupid_moves(data, 300);
376                 var response = data.pv_uci[1];
377                 for (var i = 0; i < nonstupid_moves.length; ++i) {
378                         if (nonstupid_moves[i] == data.pv_uci[0]) {
379                                 // ignore the PV move for refutation lines.
380                                 continue;
381                         }
382                         if (!data.refutation_lines ||
383                             !data.refutation_lines[nonstupid_moves[i]] ||
384                             !data.refutation_lines[nonstupid_moves[i]].pv_uci ||
385                             data.refutation_lines[nonstupid_moves[i]].pv_uci.length < 1) {
386                                 // Incomplete PV, abort.
387                                 response = undefined;
388                                 break;
389                         }
390                         var this_response = data.refutation_lines[nonstupid_moves[i]].pv_uci[1];
391                         if (response !== this_response) {
392                                 // Different response depending on lines, abort.
393                                 response = undefined;
394                                 break;
395                         }
396                 }
397
398                 if (nonstupid_moves.length > 0 && response !== undefined) {
399                         create_arrow(response.substr(0, 2),
400                                      response.substr(2, 4), '#66f', 6, 20);
401                 }
402         }
403
404         // Show the refutation lines.
405         var tbl = $("#refutationlines");
406         tbl.empty();
407
408         moves = [];
409         for (var move in data.refutation_lines) {
410                 moves.push(move);
411         }
412         moves = moves.sort(function(a, b) { return compare_by_sort_key(data, a, b) });
413         for (var i = 0; i < moves.length; ++i) {
414                 var line = data.refutation_lines[moves[i]];
415
416                 var tr = document.createElement("tr");
417
418                 var move_td = document.createElement("td");
419                 tr.appendChild(move_td);
420                 $(move_td).addClass("move");
421                 $(move_td).text(line.pretty_move);
422
423                 var score_td = document.createElement("td");
424                 tr.appendChild(score_td);
425                 $(score_td).addClass("score");
426                 $(score_td).text(line.pretty_score);
427
428                 var depth_td = document.createElement("td");
429                 tr.appendChild(depth_td);
430                 $(depth_td).addClass("depth");
431                 $(depth_td).text("d" + line.depth);
432
433                 var pv_td = document.createElement("td");
434                 tr.appendChild(pv_td);
435                 $(pv_td).addClass("pv");
436                 $(pv_td).text(print_pv(line.pv_pretty, data.position.move_num, data.position.toplay, 10));
437
438                 tbl.append(tr);
439         }
440
441         // Next update.
442         setTimeout(function() { request_update(board, 0); }, 100);
443 }
444
445 var init = function() {
446         // Create board.
447         var board = new ChessBoard('board', 'start');
448
449         request_update(board, 1);
450
451 };
452 $(document).ready(init);
453 </script>
454 </body>
455 </html>