218 lines
7.6 KiB
C++
218 lines
7.6 KiB
C++
#include <iostream>
|
|
#include <iomanip>
|
|
#include <string>
|
|
#include <fstream>
|
|
#include <sstream>
|
|
|
|
#include "utils.h"
|
|
#include "Board.h"
|
|
#include "Move.h"
|
|
#include "search.h"
|
|
#include "uci.h"
|
|
|
|
static const std::string usage{"usage: pgn2fen [--scored] [--rating <rating>] [--ply <ply>] [<input>]"};
|
|
|
|
// Convert PGN (Portable Game Notation) input stream to single FENs
|
|
// streamed to stdout
|
|
void pgn2fen(
|
|
std::istream& input,
|
|
const bool scored,
|
|
const unsigned long rating,
|
|
const unsigned long ply
|
|
) {
|
|
// Instantiate Boards, the start of every game as well as the current state
|
|
// of the Board while processing a PGN game
|
|
Board startpos, pos;
|
|
|
|
// Parse white and black ELO ratings
|
|
unsigned long whiteElo = 0;
|
|
unsigned long blackElo = 0;
|
|
|
|
// Read input line by line
|
|
std::string line;
|
|
while (std::getline(input, line)) {
|
|
// read rating metadata lines
|
|
if (rating != static_cast<unsigned long>(-1)) {
|
|
// [WhiteElo "1111"]
|
|
// [BlackElo "999"]
|
|
try {
|
|
if (line.rfind("[WhiteElo \"", 0) != std::string::npos) {
|
|
whiteElo = std::stoul(line.substr(11));
|
|
} else if (line.rfind("[BlackElo \"", 0) != std::string::npos) {
|
|
blackElo = std::stoul(line.substr(11));
|
|
}
|
|
} catch (...) {
|
|
std::cerr << "ERROR: Parsing player rating metadata '" << line
|
|
<< "' failed." << std::endl;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Skip empty and further metadata lines (every PGN game starts with "<nr>.")
|
|
if (line.empty() || line.front() == '[') {
|
|
continue;
|
|
}
|
|
|
|
// In case of rating requested, only parse game when rating is detected
|
|
if (rating != static_cast<unsigned long>(-1)
|
|
&& (whiteElo < rating || blackElo < rating)) {
|
|
continue;
|
|
}
|
|
|
|
// Reset position to the start position, every game starts here!
|
|
pos = startpos;
|
|
|
|
// Read game content (assuming one line is the entire game)
|
|
std::istringstream game(line);
|
|
std::string count, san, token, eval;
|
|
while (game >> count >> san >> token) {
|
|
// Consume/Parse PGN comments
|
|
if (scored) {
|
|
// consume the comment and search for an evaluation
|
|
bool has_score = false;
|
|
while (game >> token) {
|
|
// Search for evaluation token (position score _after_ the move)
|
|
if (token == "[%eval") {
|
|
game >> eval;
|
|
eval.pop_back(); // delete trailing ']'
|
|
has_score = true;
|
|
// Consume the remainder of the comment (ignore it)
|
|
std::getline(game, token, '}');
|
|
break;
|
|
} else if (token == "}") {
|
|
break;
|
|
}
|
|
}
|
|
// In case of not finding an evaluation, skip the game (_not_ an error)
|
|
if (!has_score) {
|
|
break;
|
|
}
|
|
} else {
|
|
// Consume the remainder of the comment (ignore it)
|
|
std::getline(game, token, '}');
|
|
}
|
|
|
|
// Perform move
|
|
bool parseError = false;
|
|
Move move = UCI::parseSAN(san, pos, parseError);
|
|
if (parseError) {
|
|
std::cerr << "ERROR: Parsing '" << san << "' at position '"
|
|
<< pos.fen() << "' failed." << std::endl;
|
|
break;
|
|
}
|
|
move = pos.isLegal(move); // validate legality and extend move info
|
|
if (move) {
|
|
pos.make(move);
|
|
} else {
|
|
std::cerr << "ERROR: Encountered illegal move '" << san
|
|
<< " (" << move
|
|
<< ") ' at position '" << pos.fen() << "'." << std::endl;
|
|
break;
|
|
}
|
|
|
|
// Skip positions with too small ply count
|
|
if (pos.plyCount() < ply) {
|
|
continue;
|
|
}
|
|
|
|
// Write positions
|
|
if (scored && rating != static_cast<unsigned long>(-1)) {
|
|
// Ingore "check mate in" scores (not relevant for eval training)
|
|
// Do this after "make move" in situations where the check mate
|
|
// was overlooked, leading to new positions
|
|
if (eval.length() && eval[0] == '#') {
|
|
continue;
|
|
}
|
|
// Otherwise, classic eval score to be parsed in centipawns
|
|
std::cout << pos.fen() << "; " << eval << "; "
|
|
<< whiteElo << "; " << blackElo << '\n';
|
|
} else if (rating != static_cast<unsigned long>(-1)) {
|
|
// Otherwise, classic eval score to be parsed in centipawns
|
|
std::cout << pos.fen() << "; " << whiteElo << "; " << blackElo << '\n';
|
|
} else if (scored) {
|
|
// Ingore "check mate in" scores (not relevant for eval training)
|
|
// Do this after "make move" in situations where the check mate
|
|
// was overlooked, leading to new positions
|
|
if (eval.length() && eval[0] == '#') {
|
|
continue;
|
|
}
|
|
// Otherwise, classic eval score to be parsed in centipawns
|
|
std::cout << pos.fen() << "; " << eval << '\n';
|
|
} else {
|
|
// Write only the position FEN
|
|
std::cout << pos.fen() << '\n';
|
|
}
|
|
|
|
}
|
|
|
|
// Reset ELO after every game to ensure that games without an elo
|
|
// metadata tag don't get the wrong rating from a previous game
|
|
whiteElo = 0;
|
|
blackElo = 0;
|
|
}
|
|
}
|
|
|
|
|
|
int main(int argn, char* argv[]) {
|
|
// Setup control variables
|
|
bool scored = false;
|
|
unsigned long rating = -1;
|
|
unsigned long ply = 0;
|
|
// unsigned min_rating = 0;
|
|
std::string file = "";
|
|
|
|
// Parse command arguments
|
|
for (int i = 1; i < argn; ++i) {
|
|
if (std::string("--scored") == argv[i]) {
|
|
scored = true;
|
|
} else if (std::string("--rating") == argv[i]) {
|
|
if (i + 1 < argn) {
|
|
try {
|
|
rating = std::stoul(argv[++i]);
|
|
} catch (...) {
|
|
std::cerr << "ERROR: illegal --rating argument " << argv[i] << std::endl;
|
|
std::cout << usage << std::endl;
|
|
return 1;
|
|
}
|
|
} else {
|
|
std::cout << usage << std::endl;
|
|
return 1;
|
|
}
|
|
} else if (std::string("--ply") == argv[i]) {
|
|
if (i + 1 < argn) {
|
|
try {
|
|
ply = std::stoul(argv[++i]);
|
|
} catch (...) {
|
|
std::cerr << "ERROR: illegal --ply argument " << argv[i] << std::endl;
|
|
std::cout << usage << std::endl;
|
|
return 1;
|
|
}
|
|
} else {
|
|
std::cout << usage << std::endl;
|
|
return 1;
|
|
}
|
|
} else if (file != "") {
|
|
file = argv[i];
|
|
} else {
|
|
std::cout << usage << std::endl;
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
// Invoke converter ether with file input or stdin
|
|
if (file == "") {
|
|
pgn2fen(std::cin, scored, rating, ply);
|
|
} else {
|
|
// Open input file
|
|
std::ifstream input(file);
|
|
if (!input) {
|
|
std::cerr << "ERROR: opening '" << file << "' failed" << std::endl;
|
|
return 1;
|
|
}
|
|
|
|
pgn2fen(input, scored, rating, ply);
|
|
}
|
|
|
|
return 0;
|
|
}
|