1 /****************************************************************************
2 *   Copyright (C) 2014 by Jens Nissen jens-chessx@gmx.net                   *
3 ****************************************************************************/
4 
5 #include "guess.h"
6 #include "board.h"
7 #include "ecopositions.h"
8 
9 #include <QDataStream>
10 #include <QDebug>
11 #include <QDir>
12 #include <QFile>
13 #include <QFileInfo>
14 #include <QList>
15 
16 using namespace chessx;
17 
18 #if defined(_MSC_VER) && defined(_DEBUG)
19 #define DEBUG_NEW new( _NORMAL_BLOCK, __FILE__, __LINE__ )
20 #define new DEBUG_NEW
21 #endif // _MSC_VER
22 
23 // Parsed data containers
24 static QMap<quint64, QString> ecoPositions;
25 static QMap<quint64, QString> ecoNames;
26 static QMap<quint64, QList<Square> > gtmPositions;
27 
28 // Number of milliseconds to spend deciding which of two possible moves
29 //  is the better one for the guess-the-move feature to offer user
30 #define CALCULATE_MS	20
31 
32 // Create GTM (Guess-The-Move) hash data.
33 //
34 // This new hash has a key on "position + square" and contains the move
35 // that the guess-the-move feature should display when the user hovers
36 // the mouse over this square in this position.
37 //
38 // It can be looked up quickly by the guess-the-move feature by
39 // combining the hash for the current position + a unique hash for each
40 // of the 64 squares.  Thus providing an individual hash key for each
41 // ECO position / target square combination.
42 //
43 // Because we can only offer the user a single choice, if the ECO data
44 // suggests more than one possibility for a given position-square combo,
45 // then use the Scid internal chess engine to decide which is the "best"
46 // move to display to the user.
47 //
updateFinalGuess(const BoardX & board,Square target,const Move & move)48 void updateFinalGuess(const BoardX& board, Square target, const Move& move)
49 {
50     Square from = move.from();
51     Square to = move.to();
52 
53     QList<Square> data;
54     data << from << to;
55 
56     quint64 key = board.getHashPlusSquare(target);
57     if(gtmPositions.contains(key))
58     {
59         int ofrom = gtmPositions[key][0];
60         int oto = gtmPositions[key][1];
61         // For some reason the ECO data has a few dupes
62         if(from == ofrom && to == oto)
63         {
64             return;
65         }
66         int ret = Guess::pickBest(
67                       qPrintable(board.toFen()),
68                       false, /* chess960 not supported */
69                       BitBoard::standardCastlingRooks(),
70                       (Guess::squareT)from, (Guess::squareT)to, (Guess::squareT)ofrom, (Guess::squareT)oto,
71                       CALCULATE_MS);
72         if(ret == 0)
73         {
74             gtmPositions[key] = data;
75         }
76         else if(ret < 0)
77         {
78             qDebug() << "ERROR deciding between moves..";
79             qDebug() << board.toFen()
80                      << target
81                      << from << to
82                      << gtmPositions[key][0] << gtmPositions[key][1];
83         }
84     }
85     else
86     {
87         gtmPositions[key] = data;
88     }
89 }
90 
parseAsciiEcoData(const QString & ecoFile)91 bool parseAsciiEcoData(const QString& ecoFile)
92 {
93     ecoPositions.clear();
94     ecoNames.clear();
95 
96     QFile file(ecoFile);
97     if(!file.open(QIODevice::ReadOnly))
98     {
99         return false;
100     }
101     QTextStream ecoStream(&file);
102     ecoStream.setCodec("ISO-8859-1");
103 
104     QString line;
105     BoardX board;
106     QString ecoCode;
107     QRegExp ecoRegExp("[A-Z]\\d{2}[a-z]?");
108     QStringList tokenList;
109     QString token;
110     Move move;
111 
112     while(!ecoStream.atEnd())
113     {
114         line = ecoStream.readLine();
115 
116         //ignore comments and blank lines
117         if(line.startsWith("#") || line == "")
118         {
119             continue;
120         }
121 
122         //if line starts with eco code, store and begin new line
123         if(line.indexOf(ecoRegExp) == 0)
124         {
125             ecoCode = line.section(' ', 0, 0);
126             ecoCode += " " + line.section('"', 1, 1);
127             board.setStandardPosition();
128             line = line.section('"', 2);
129         }
130 
131         //parse any moves on line
132         tokenList = line.split(" ");
133         for(QStringList::ConstIterator iterator = tokenList.constBegin(); iterator != tokenList.constEnd(); iterator++)
134         {
135             token = *iterator;
136             if(token == "*")
137             {
138                 // Record final position of this variation along with its ECO code
139                 ecoPositions.insert(board.getHashValue(), ecoCode);
140 
141                 if(!move.isLegal())
142                 {
143                     continue;
144                 }
145 
146                 // Guess the move is based on second-to-last move, so undo last move
147                 BoardX guess(board);
148                 guess.undoMove(move);
149 
150                 // We update twice because user might put mouse over
151                 //  the "to" or "from" squares.
152                 updateFinalGuess(guess, move.to(), move);
153                 updateFinalGuess(guess, move.from(), move);
154 
155                 continue;
156             }
157             if(token.contains('.'))
158             {
159                 token = token.section('.', 1, 1);
160             }
161             if(token != "")
162             {
163                 move = board.parseMove(token);
164                 if(move.isLegal())
165                 {
166                     board.doMove(move);
167                 }
168                 else
169                 {
170                     qDebug() << "failed on" << token;
171                     ecoPositions.clear();
172                     return false;
173                 }
174             }
175         }
176     }
177 
178     return true;
179 }
180 
181 // "chessx.eco.txt" -> "chessx.eco", "chessx.gtm", "chessx.txt"
compileAsciiEcoFile(const QString & filenameIn,QString filenameOut,QString gtmFile)182 bool compileAsciiEcoFile(const QString& filenameIn, QString filenameOut, QString gtmFile)
183 {
184     // Read in the ECO data
185     if(!parseAsciiEcoData(filenameIn))
186     {
187         return false;
188     }
189 
190     filenameOut = QFileInfo(filenameIn).absolutePath() + QDir::separator() + filenameOut;
191     gtmFile = QFileInfo(filenameIn).absolutePath() + QDir::separator() + gtmFile;
192 
193     // Write out the main ECO file
194     QFile file(filenameOut);
195     file.open(QIODevice::WriteOnly);
196     QDataStream sout(&file);
197     sout << COMPILED_ECO_FILE_ID;
198     sout << ecoPositions;
199     file.close();
200 
201     // Write out the GTM (guess-the-move) ECO file
202     QFile gfile(gtmFile);
203     gfile.open(QIODevice::WriteOnly);
204     QDataStream gout(&gfile);
205     gout << COMPILED_GUESS_FILE_ID;
206     gout << gtmPositions;
207     gfile.close();
208 
209     ecoPositions.clear();
210     gtmPositions.clear();
211 
212     return true;
213 }
214