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