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