Fix sorting of Engine Output
[xboard.git] / engineoutput.c
1 /*
2  * engineoutput.c - split-off backe-end from Engine output (PV) by HGM
3  *
4  * Author: Alessandro Scotti (Dec 2005)
5  *
6  * Copyright 2005 Alessandro Scotti
7  *
8  * Enhancements Copyright 1995, 2009, 2010, 2011, 2012, 2013, 2014 Free Software Foundation, Inc.
9  *
10  * ------------------------------------------------------------------------
11  *
12  * GNU XBoard is free software: you can redistribute it and/or modify
13  * it under the terms of the GNU General Public License as published by
14  * the Free Software Foundation, either version 3 of the License, or (at
15  * your option) any later version.
16  *
17  * GNU XBoard is distributed in the hope that it will be useful, but
18  * WITHOUT ANY WARRANTY; without even the implied warranty of
19  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
20  * General Public License for more details.
21  *
22  * You should have received a copy of the GNU General Public License
23  * along with this program. If not, see http://www.gnu.org/licenses/.  *
24  *
25  *------------------------------------------------------------------------
26  ** See the file ChangeLog for a revision history.  */
27
28 #define SHOW_PONDERING
29
30 #include "config.h"
31
32 #include <stdio.h>
33
34 #if STDC_HEADERS
35 # include <stdlib.h>
36 # include <string.h>
37 #else /* not STDC_HEADERS */
38 # if HAVE_STRING_H
39 #  include <string.h>
40 # else /* not HAVE_STRING_H */
41 #  include <strings.h>
42 # endif /* not HAVE_STRING_H */
43 #endif /* not STDC_HEADERS */
44
45 #include "common.h"
46 #include "frontend.h"
47 #include "backend.h"
48 #include "moves.h"
49 #include "engineoutput.h"
50 #include "gettext.h"
51
52 #ifdef ENABLE_NLS
53 # define  _(s) gettext (s)
54 # define N_(s) gettext_noop (s)
55 #else
56 # ifdef WIN32
57 #  define  _(s) T_(s)
58 #  undef  ngettext
59 #  define  ngettext(s,p,n) T_(p)
60 # else
61 #  define  _(s) (s)
62 # endif
63 # define N_(s)  s
64 #endif
65
66 typedef struct {
67     char * name;
68     int which;
69     int depth;
70     u64 nodes;
71     int score;
72     int time;
73     char * pv;
74     char * hint;
75     int an_move_index;
76     int an_move_count;
77     int moveKey;
78 } EngineOutputData;
79
80 // called by other front-end
81 void EngineOutputUpdate( FrontEndProgramStats * stats );
82 void OutputKibitz(int window, char *text);
83
84 // module back-end routines
85 static void VerifyDisplayMode();
86 static void UpdateControls( EngineOutputData * ed );
87
88 static int  lastDepth[2] = { -1, -1 };
89 static int  lastForwardMostMove[2] = { -1, -1 };
90 static int  engineState[2] = { -1, -1 };
91 static char lastLine[2][MSG_SIZ];
92 static char header[2][MSG_SIZ];
93 static char columnHeader[MSG_SIZ] = "dep\tscore\tnodes\ttime\t(not shown:  tbhits\tknps\tseldep)\n";
94 static int  columnMask = 0xF0;
95
96 #define MAX_VAR 400
97 static int scores[MAX_VAR], textEnd[MAX_VAR], keys[MAX_VAR], curDepth[2], nrVariations[2];
98
99 extern int initialRulePlies;
100
101 void
102 MakeEngineOutputTitle ()
103 {
104         static char buf[MSG_SIZ];
105         static char oldTitle[MSG_SIZ];
106         char title[MSG_SIZ];
107         int count, rule = 2*appData.ruleMoves;
108
109         snprintf(title, MSG_SIZ, _("Engine Output") );
110
111         if(!EngineOutputIsUp()) return;
112         // figure out value of 50-move counter
113         count = currentMove;
114         while( (signed char)boards[count][EP_STATUS] <= EP_NONE && count > backwardMostMove ) count--;
115         if( count == backwardMostMove ) count -= initialRulePlies;
116         count = currentMove - count;
117         if(!rule) rule = 100;
118         if(count >= rule - 40 && (!appData.icsActive || gameMode == IcsObserving || appData.zippyPlay)) {
119                 snprintf(buf, MSG_SIZ, ngettext("%s (%d reversible ply)", "%s (%d reversible plies)", count), title, count);
120                 safeStrCpy(title, buf, MSG_SIZ);
121         }
122         if(!strcmp(oldTitle, title)) return;
123         safeStrCpy(oldTitle, title, MSG_SIZ);
124         SetEngineOutputTitle(title);
125 }
126
127 // back end, due to front-end wrapper for SetWindowText, and new SetIcon arguments
128 void
129 SetEngineState (int which, enum ENGINE_STATE state, char * state_data)
130 {
131     int x_which = 1 - which;
132
133     if( engineState[ which ] != state ) {
134         engineState[ which ] = state;
135
136         switch( state ) {
137         case STATE_THINKING:
138             SetIcon( which, nStateIcon, nThinking );
139             if( engineState[ x_which ] == STATE_THINKING ) {
140                 SetEngineState( x_which, STATE_IDLE, "" );
141             }
142             break;
143         case STATE_PONDERING:
144             SetIcon( which, nStateIcon, nPondering );
145             break;
146         case STATE_ANALYZING:
147             SetIcon( which, nStateIcon, nAnalyzing );
148             break;
149         default:
150             SetIcon( which, nStateIcon, nClear );
151             break;
152         }
153     }
154
155     if( state_data != 0 ) {
156         DoSetWindowText( which, nStateData, state_data );
157     }
158 }
159
160 // back end, now the front-end wrapper ClearMemo is used, and ed no longer contains handles.
161 void
162 SetProgramStats (FrontEndProgramStats * stats) // now directly called by back-end
163 {
164     EngineOutputData ed;
165     int clearMemo = FALSE;
166     int which, depth, multi;
167     ChessMove moveType;
168     int ff, ft, rf, rt;
169     char pc;
170
171     if( stats == 0 ) {
172         SetEngineState( 0, STATE_IDLE, "" );
173         SetEngineState( 1, STATE_IDLE, "" );
174         return;
175     }
176
177     if(gameMode == IcsObserving && !appData.icsEngineAnalyze)
178         return; // [HGM] kibitz: shut up engine if we are observing an ICS game
179
180     which = stats->which;
181     depth = stats->depth;
182
183     if( which < 0 || which > 1 || depth < 0 || stats->time < 0 || stats->pv == 0 ) {
184         return;
185     }
186
187     if( !EngineOutputDialogExists() ) {
188         return;
189     }
190
191     VerifyDisplayMode();
192
193     ed.which = which;
194     ed.depth = depth;
195     ed.nodes = stats->nodes;
196     ed.score = stats->score;
197     ed.time = stats->time;
198     ed.pv = stats->pv;
199     ed.hint = stats->hint;
200     ed.an_move_index = stats->an_move_index;
201     ed.an_move_count = stats->an_move_count;
202
203     /* Get target control. [HGM] this is moved to front end, which get them from a table */
204     if( which == 0 ) {
205         ed.name = first.tidy;
206     }
207     else {
208         ed.name = second.tidy;
209     }
210
211     if( ed.pv != 0 && ed.pv[0] == ' ' ) {
212         if( strncmp( ed.pv, " no PV", 6 ) == 0 ) { /* Hack on hack! :-O */
213             ed.pv = "";
214         }
215     }
216
217     /* Clear memo if needed */
218     if( lastDepth[which] > depth || (lastDepth[which] == depth && depth <= 1 && ed.pv[0]) ) { // no reason to clear if we won't add line
219         clearMemo = TRUE;
220     }
221
222     if( lastForwardMostMove[which] != forwardMostMove ) {
223         clearMemo = TRUE;
224     }
225
226     if( clearMemo ) {
227         if(!appData.headers) columnHeader[0] = NULLCHAR;
228         DoClearMemo(which); nrVariations[which] = 0;
229         header[which][0] = NULLCHAR;
230         if(gameMode == AnalyzeMode) {
231           ChessProgramState *cps = (which ? &second : &first);
232           if((multi = MultiPV(cps)) >= 0) {
233             snprintf(header[which], MSG_SIZ, "\t%s viewpoint\t\tfewer / Multi-PV setting = %d / more\n",
234                                        appData.whitePOV || appData.scoreWhite ? "white" : "mover", cps->option[multi].value);
235           }
236           if(!which) snprintf(header[which]+strlen(header[which]), MSG_SIZ-strlen(header[which]), "%s%s", exclusionHeader, columnHeader);
237           InsertIntoMemo( which, header[which], 0);
238         } else {
239           snprintf(header[which], MSG_SIZ, "%s", columnHeader);
240           if(appData.ponderNextMove && lastLine[which][0]) {
241             InsertIntoMemo( which, lastLine[which], 0 );
242             InsertIntoMemo( which, "\n", 0 );
243           }
244           InsertIntoMemo( which, header[which], 0);
245         }
246     }
247
248     if(ed.pv && ed.pv[0] && ParseOneMove(ed.pv, currentMove, &moveType, &ff, &rf, &ft, &rt, &pc))
249         ed.moveKey = (ff<<24 | rf << 16 | ft << 8 | rt) ^ pc*87161;
250     else ed.moveKey = ed.nodes; // kludge to get unique key unlikely to match any move
251
252     /* Update */
253     lastDepth[which] = depth == 1 && ed.nodes == 0 ? 0 : depth; // [HGM] info-line kudge
254     lastForwardMostMove[which] = forwardMostMove;
255
256     UpdateControls( &ed );
257 }
258
259 #define ENGINE_COLOR_WHITE      'w'
260 #define ENGINE_COLOR_BLACK      'b'
261 #define ENGINE_COLOR_UNKNOWN    ' '
262
263 // pure back end
264 static char
265 GetEngineColor (int which)
266 {
267     char result = ENGINE_COLOR_UNKNOWN;
268
269     if( which == 0 || which == 1 ) {
270         ChessProgramState * cps;
271
272         switch (gameMode) {
273         case MachinePlaysBlack:
274         case IcsPlayingBlack:
275             result = ENGINE_COLOR_BLACK;
276             break;
277         case MachinePlaysWhite:
278         case IcsPlayingWhite:
279             result = ENGINE_COLOR_WHITE;
280             break;
281         case AnalyzeMode:
282         case AnalyzeFile:
283             result = WhiteOnMove(forwardMostMove) ? ENGINE_COLOR_WHITE : ENGINE_COLOR_BLACK;
284             break;
285         case TwoMachinesPlay:
286             cps = (which == 0) ? &first : &second;
287             result = cps->twoMachinesColor[0];
288             result = result == 'w' ? ENGINE_COLOR_WHITE : ENGINE_COLOR_BLACK;
289             break;
290         default: ; // does not happen, but suppresses pedantic warnings
291         }
292     }
293
294     return result;
295 }
296
297 // pure back end
298 static char
299 GetActiveEngineColor ()
300 {
301     char result = ENGINE_COLOR_UNKNOWN;
302
303     if( gameMode == TwoMachinesPlay ) {
304         result = WhiteOnMove(forwardMostMove) ? ENGINE_COLOR_WHITE : ENGINE_COLOR_BLACK;
305     }
306
307     return result;
308 }
309
310 // pure back end
311 static int
312 IsEnginePondering (int which)
313 {
314     int result = FALSE;
315
316     switch (gameMode) {
317     case MachinePlaysBlack:
318     case IcsPlayingBlack:
319         if( WhiteOnMove(forwardMostMove) ) result = TRUE;
320         break;
321     case MachinePlaysWhite:
322     case IcsPlayingWhite:
323         if( ! WhiteOnMove(forwardMostMove) ) result = TRUE;
324         break;
325     case TwoMachinesPlay:
326         if( GetActiveEngineColor() != ENGINE_COLOR_UNKNOWN ) {
327             if( GetEngineColor( which ) != GetActiveEngineColor() ) result = TRUE;
328         }
329         break;
330     default: ; // does not happen, but suppresses pedantic warnings
331     }
332
333     return result;
334 }
335
336 // back end
337 static void
338 SetDisplayMode (int mode)
339 {
340     if( windowMode != mode ) {
341         windowMode = mode;
342
343         ResizeWindowControls( mode );
344     }
345 }
346
347 // pure back end
348 static void
349 VerifyDisplayMode ()
350 {
351     int mode;
352
353     /* Get proper mode for current game */
354     switch( gameMode ) {
355     case IcsObserving:    // [HGM] ICS analyze
356         if(!appData.icsEngineAnalyze) return;
357     case AnalyzeFile:
358     case MachinePlaysWhite:
359     case MachinePlaysBlack:
360         mode = 0;
361         break;
362     case AnalyzeMode:
363         mode = second.analyzing;
364         break;
365     case IcsPlayingWhite:
366     case IcsPlayingBlack:
367         mode = appData.zippyPlay && opponentKibitzes; // [HGM] kibitz
368         break;
369     case TwoMachinesPlay:
370         mode = 1;
371         break;
372     default:
373         /* Do not change */
374         return;
375     }
376
377     SetDisplayMode( mode );
378 }
379
380 // back end. Determine what icon to set in the color-icon field, and print it
381 void
382 SetEngineColorIcon (int which)
383 {
384     char color = GetEngineColor(which);
385     int nicon = 0;
386
387     if( color == ENGINE_COLOR_BLACK )
388         nicon = nColorBlack;
389     else if( color == ENGINE_COLOR_WHITE )
390         nicon = nColorWhite;
391     else
392         nicon = nColorUnknown;
393
394     SetIcon( which, nColorIcon, nicon );
395 }
396
397 #define MAX_NAME_LENGTH 32
398
399 // [HGM] multivar: sort Thinking Output within one depth on score
400
401 static int
402 InsertionPoint (int len, EngineOutputData *ed)
403 {
404         int i, offs = 0, newScore = ed->score, n = ed->which;
405
406         if(ed->nodes == 0 && ed->score == 0 && ed->time == 0)
407                 newScore = 1e6; // info lines inserted on top
408         if(ed->depth != curDepth[n]) { // depth has changed
409                 curDepth[n] = ed->depth;
410                 nrVariations[n] = 0; // throw away everything we had
411         }
412         // loop through all lines. Note even / odd used for different panes
413         for(i=nrVariations[n]-2; i>=0; i-=2) {
414                 // put new item behind those we haven't looked at
415                 offs = textEnd[i+n];
416                 textEnd[i+n+2] = offs + len;
417                 scores[i+n+2] = newScore;
418                 keys[i+n+2] = ed->moveKey;
419                 if(ed->moveKey != keys[i+n] && // same move always tops previous one (as a higher score must be a fail low)
420                    newScore < scores[i+n]) break;
421                 // if it had higher score as previous, move previous in stead
422                 scores[i+n+2] = ed->moveKey == keys[i+n] ? newScore : scores[i+n]; // correct scores of fail-low/high searches
423                 textEnd[i+n+2] = textEnd[i+n] + len;
424                 keys[i+n+2] = keys[i+n];
425         }
426         if(i<0) {
427                 offs = 0;
428                 textEnd[n] = offs + len;
429                 scores[n] = newScore;
430                 keys[n] = ed->moveKey;
431         }
432         nrVariations[n] += 2;
433       return offs + strlen(header[ed->which]);
434 }
435
436 static char spaces[] = "            "; // [HGM] align: spaces for padding
437
438 static void
439 Format(char *buf, int val)
440 { // [HGM] tbhits: print a positive integer with trailing whitespace to give it fixed width
441         if( val < 1000000 ) {
442             int h = val, i=0;
443             while(h > 0) h /= 10, i++;
444             snprintf( buf, 24, "%d%s\t", val, spaces + 2*i);
445         }
446         else {
447             snprintf( buf, 24, "%.1fM%s\t", val/1000000.0, spaces + 8 + 2*(val > 1e7));
448         }
449 }
450
451 // pure back end, now SetWindowText is called via wrapper DoSetWindowText
452 static void
453 UpdateControls (EngineOutputData *ed)
454 {
455 //    int isPondering = FALSE;
456
457     char s_label[MAX_NAME_LENGTH + 32];
458     int h;
459     char * name = ed->name;
460
461     /* Label */
462     if( name == 0 || *name == '\0' ) {
463         name = "?";
464     }
465
466     strncpy( s_label, name, MAX_NAME_LENGTH );
467     s_label[ MAX_NAME_LENGTH-1 ] = '\0';
468
469 #ifdef SHOW_PONDERING
470     if( IsEnginePondering( ed->which ) ) {
471         char buf[12];
472
473         buf[0] = '\0';
474
475         if( ed->hint != 0 && *ed->hint != '\0' ) {
476             strncpy( buf, ed->hint, sizeof(buf) );
477             buf[sizeof(buf)-1] = '\0';
478         }
479         else if( ed->pv != 0 && *ed->pv != '\0' ) {
480             char * sep, *startPV = ed->pv, c;
481             int buflen = sizeof(buf);
482
483             if(sscanf(ed->pv, "{%*d,%*d,%*d}%c", &c) && c == ' ') startPV = strchr(ed->pv, '}') + 2; // [HGM] tbhits
484             sep = strchr( startPV, ' ' );
485             if( sep != NULL ) {
486                 buflen = sep - startPV + 1;
487                 if( buflen > sizeof(buf) ) buflen = sizeof(buf);
488             }
489
490             strncpy( buf, startPV, buflen );
491             buf[ buflen-1 ] = '\0';
492         }
493
494         SetEngineState( ed->which, STATE_PONDERING, buf );
495     }
496     else if( gameMode == TwoMachinesPlay ) {
497         SetEngineState( ed->which, STATE_THINKING, "" );
498     }
499     else if( gameMode == AnalyzeMode || gameMode == AnalyzeFile
500           || (gameMode == IcsObserving && appData.icsEngineAnalyze)) { // [HGM] ICS-analyze
501         char buf[64];
502         int time_secs = ed->time / 100;
503         int time_mins = time_secs / 60;
504
505         buf[0] = '\0';
506
507         if( ed->an_move_index != 0 && ed->an_move_count != 0 && *ed->hint != '\0' ) {
508             char mov[16];
509
510             strncpy( mov, ed->hint, sizeof(mov) );
511             mov[ sizeof(mov)-1 ] = '\0';
512
513             snprintf( buf, sizeof(buf)/sizeof(buf[0]), "[%d] %d/%d: %s [%02d:%02d:%02d]", ed->depth, ed->an_move_index,
514                         ed->an_move_count, mov, time_mins / 60, time_mins % 60, time_secs % 60 );
515         }
516
517         SetEngineState( ed->which, STATE_ANALYZING, buf );
518     }
519     else {
520         SetEngineState( ed->which, STATE_IDLE, "" );
521     }
522 #endif
523
524     DoSetWindowText( ed->which, nLabel, s_label );
525
526     s_label[0] = '\0';
527
528     if( ed->time > 0 && ed->nodes > 0 ) {
529         unsigned long nps_100 = ed->nodes / ed->time;
530
531         if( nps_100 < 100000 ) {
532           snprintf( s_label, sizeof(s_label)/sizeof(s_label[0]), "%s: %lu", _("NPS"), nps_100 * 100 );
533         }
534         else {
535           snprintf( s_label, sizeof(s_label)/sizeof(s_label[0]), "%s: %.1fk", _("NPS"), nps_100 / 10.0 );
536         }
537     }
538
539     DoSetWindowText( ed->which, nLabelNPS, s_label );
540
541     /* Memo */
542     if( ed->pv != 0 && *ed->pv != '\0' ) {
543         char s_nodes[24];
544         char s_score[16];
545         char s_time[24];
546         char s_hits[24];
547         char s_seld[24];
548         char s_knps[24];
549         char buf[256], *pvStart = ed->pv, fail;
550         int buflen, hits, seldep, knps, extra;
551         int time_secs = ed->time / 100;
552         int time_cent = ed->time % 100;
553
554         /* Nodes */
555         if( ed->nodes < 1000000 ) {
556             int h = ed->nodes, i=0;
557             while(h > 0) h /= 10, i++; // [HGM] align: count digits; pad with 2 spaces for every missing digit
558             snprintf( s_nodes, sizeof(s_nodes)/sizeof(s_nodes[0]), u64Display "%s\t", ed->nodes, spaces + 2*i);
559         }
560         else {
561             snprintf( s_nodes, sizeof(s_nodes)/sizeof(s_nodes[0]), "%.1fM%s\t", u64ToDouble(ed->nodes) / 1000000.0,
562                       spaces + 8 + 2*(ed->nodes > 1e7));
563         }
564
565         /* TB Hits etc. */
566         hits = knps = seldep = 0; extra = sscanf(ed->pv, "{%d,%d,%d", &seldep, &knps, &hits);
567         Format(s_seld, seldep); Format(s_knps, knps); Format(s_hits, hits); 
568         if(extra) { // strip extended info from PV
569             if((pvStart = strstr(ed->pv, "} "))) pvStart += 2; else pvStart = ed->pv;
570         }
571         fail = ed->pv[strlen(ed->pv)-1];
572         if(fail != '?' && fail != '!') fail = ' ';
573
574         /* Score */
575         h = ((gameMode == AnalyzeMode && appData.whitePOV || appData.scoreWhite) && !WhiteOnMove(currentMove) ? -1 : 1) * ed->score;
576         if( h == 0 ) {
577           snprintf( s_score, sizeof(s_score)/sizeof(s_score[0]), "  0.00%c\t", fail );
578         } else
579         if( h > 0 ) {
580           snprintf( s_score, sizeof(s_score)/sizeof(s_score[0]), "+%.2f%c\t", h / 100.0, fail );
581         }
582         else {
583           snprintf( s_score, sizeof(s_score)/sizeof(s_score[0]), " %.2f%c\t", h / 100.0, fail );
584         }
585
586         /* Time */
587         snprintf( s_time, sizeof(s_time)/sizeof(s_time[0]), "%d:%02d.%02d\t", time_secs / 60, time_secs % 60, time_cent );
588
589         if(columnMask & 2) s_score[0] = NULLCHAR; // [HGM] hide: erase columns the user has hidden
590         if(columnMask & 4) s_nodes[0] = NULLCHAR;
591         if(columnMask & 8) s_time[0]  = NULLCHAR;
592         if(columnMask & 16) s_hits[0]  = NULLCHAR;
593         if(columnMask & 32) s_knps[0]  = NULLCHAR;
594         if(columnMask & 64) s_seld[0]  = NULLCHAR;
595
596         /* Put all together... */
597         if(ed->nodes == 0 && ed->score == 0 && ed->time == 0)
598           snprintf( buf, sizeof(buf)/sizeof(buf[0]), "%3d\t", ed->depth );
599         else
600           snprintf( buf, sizeof(buf)/sizeof(buf[0]), "%3d\t%s%s%s%s%s%s", ed->depth, s_score, s_nodes, s_time, s_hits, s_knps, s_seld );
601
602         /* Add PV */
603         buflen = strlen(buf);
604
605         strncpy( buf + buflen, pvStart, sizeof(buf) - buflen );
606
607         buf[ sizeof(buf) - 3 ] = '\0';
608
609         strcat( buf + buflen, "\r\n" );
610
611         /* Update memo */
612         InsertIntoMemo( ed->which, buf, InsertionPoint(strlen(buf), ed) );
613         strncpy(lastLine[ed->which], buf, MSG_SIZ);
614     }
615
616     /* Colors */
617     SetEngineColorIcon( ed->which );
618 }
619
620 static char *titles[] = { "score\t", "nodes\t", "time\t", "tbhits\t", "knps\t", "seldep\t" };
621
622 void
623 Collapse(int n)
624 {   // handle click on column headers, to hide / show them
625     int i, j, nr=0, m=~columnMask, Ncol=7;
626     for(i=0; columnHeader[i] && i<n; i++) nr += (columnHeader[i] == '\t');
627     if(!nr) return; // depth always shown, so clicks on it ignored
628     for(i=j=0; i<Ncol; i++) if(m & 1<<i) j++; // count hidden columns
629     if(nr < j) { // shown column clicked: hide it
630         for(i=j=0; i<Ncol; i++) if(m & 1<<i && j++ == nr) break;
631         columnMask |= 1<<i;
632     } else { // hidden column clicked: show it
633         m = ~m; nr -= j;
634         for(i=j=0; i<Ncol; i++) if(m & 1<<i && j++ == nr) break;
635         columnMask &= ~(1<<i);
636     }
637     // create new header line
638     strcpy(columnHeader, "dep\t");
639     m = ~columnMask;
640     for(i=j=1; i<Ncol; i++) if(m & 1<<i) strcat(columnHeader, titles[i-1]), j++;
641     if(j != Ncol) { // list hidden columns, so user ca click them
642         m = ~m; strcat(columnHeader, "(not shown:  ");
643         for(i=1; i<Ncol; i++) if(m & 1<<i) strcat(columnHeader, titles[i-1]);
644         strcat(columnHeader, ")");
645     }
646     strcat(columnHeader, "\n");
647 }
648
649 // [HGM] kibitz: write kibitz line; split window for it if necessary
650 void
651 OutputKibitz (int window, char *text)
652 {
653         static int currentLineEnd[2];
654         int where = 0;
655         if(!EngineOutputIsUp()) return;
656         if(!opponentKibitzes) { // on first kibitz of game, clear memos
657             DoClearMemo(1); currentLineEnd[1] = 0;
658             if(gameMode == IcsObserving) { DoClearMemo(0); currentLineEnd[0] = 0; }
659         }
660         opponentKibitzes = TRUE; // this causes split window DisplayMode in ICS modes.
661         VerifyDisplayMode();
662         strncpy(text+strlen(text)-1, "\r\n",sizeof(text+strlen(text)-1)); // to not lose line breaks on copying
663         if(gameMode == IcsObserving) {
664             DoSetWindowText(0, nLabel, gameInfo.white);
665             SetIcon( 0, nColorIcon,  nColorWhite);
666             SetIcon( 0, nStateIcon,  nClear);
667         }
668         DoSetWindowText(1, nLabel, gameMode == IcsPlayingBlack ? gameInfo.white : gameInfo.black); // opponent name
669         SetIcon( 1, nColorIcon,  gameMode == IcsPlayingBlack ? nColorWhite : nColorBlack);
670         SetIcon( 1, nStateIcon,  nClear);
671         if(strstr(text, "\\  ") == text) where = currentLineEnd[window-1]; // continuation line
672 //if(appData.debugMode) fprintf(debugFP, "insert '%s' at %d (end = %d,%d)\n", text, where, currentLineEnd[0], currentLineEnd[1]);
673         InsertIntoMemo(window-1, text, where); // [HGM] multivar: always at top
674         currentLineEnd[window-1] = where + strlen(text);
675 }