gnome-chess/lib/chess-pgn.vala

537 lines
17 KiB
Vala
Raw Normal View History

2014-01-07 02:19:07 +00:00
/* -*- Mode: vala; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
*
2016-03-15 17:12:58 +00:00
* Copyright (C) 2010-2014 Robert Ancell
* Copyright (C) 2015-2016 Sahil Sareen
2013-07-07 21:14:29 +00:00
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation, either version 3 of the License, or (at your option) any later
2013-07-07 21:14:29 +00:00
* version. See http://www.gnu.org/copyleft/gpl.html the full text of the
* license.
*/
/* This is the Seven Tag Roster (STR). They have to appear at the top, in this order. */
private int str_index (string name)
{
if (name == "Event")
return 0;
else if (name == "Site")
return 1;
else if (name == "Date")
return 2;
else if (name == "Round")
return 3;
else if (name == "White")
return 4;
else if (name == "Black")
return 5;
else if (name == "Result")
return 6;
else
return 7;
}
private int compare_tag (string name0, string name1)
{
int str_index0 = str_index (name0);
int str_index1 = str_index (name1);
/* If both not in STR then just order alphabetically */
if (str_index0 == 7 && str_index1 == 7)
return strcmp (name0, name1);
else
return str_index0 - str_index1;
}
2011-01-01 02:42:03 +00:00
public errordomain PGNError
{
LOAD_ERROR
}
2014-06-24 13:14:59 +00:00
public class PGNGame : Object
{
2011-01-01 01:42:50 +00:00
public HashTable<string, string> tags;
public List<string> moves;
2011-01-01 03:24:41 +00:00
public static string RESULT_IN_PROGRESS = "*";
public static string RESULT_DRAW = "1/2-1/2";
public static string RESULT_WHITE = "1-0";
public static string RESULT_BLACK = "0-1";
public static string TERMINATE_ABANDONED = "abandoned";
public static string TERMINATE_ADJUDICATION = "adjudication";
public static string TERMINATE_DEATH = "death";
public static string TERMINATE_EMERGENCY = "emergency";
public static string TERMINATE_NORMAL = "normal";
public static string TERMINATE_RULES_INFRACTION = "rules infraction";
public static string TERMINATE_TIME_FORFEIT = "time forfeit";
public static string TERMINATE_UNTERMINATED = "unterminated";
public string event
{
get { return tags.lookup ("Event"); }
set { tags.insert ("Event", value); }
}
public string site
{
get { return tags.lookup ("Site"); }
set { tags.insert ("Site", value); }
}
public string date
{
get { return tags.lookup ("Date"); }
set { tags.insert ("Date", value); }
}
2011-01-09 03:25:33 +00:00
public string time
{
get { return tags.lookup ("Time"); }
set { tags.insert ("Time", value); }
}
public string round
{
get { return tags.lookup ("Round"); }
set { tags.insert ("Round", value); }
}
public string white
{
get { return tags.lookup ("White"); }
set { tags.insert ("White", value); }
}
public string black
{
get { return tags.lookup ("Black"); }
set { tags.insert ("Black", value); }
}
public string result
{
get { return tags.lookup ("Result"); }
set { tags.insert ("Result", value); }
}
2010-12-27 02:31:07 +00:00
public string? annotator
{
get { return tags.lookup ("Annotator"); }
set { tags.insert ("Annotator", value); }
}
2011-01-09 03:25:33 +00:00
public string? time_control
{
get { return tags.lookup ("TimeControl"); }
set { tags.insert ("TimeControl", value); }
}
public string? white_time_left
{
get { return tags.lookup ("X-GNOME-WhiteTimeLeft"); }
set { tags.insert ("X-GNOME-WhiteTimeLeft", value); }
}
public string? black_time_left
{
get { return tags.lookup ("X-GNOME-BlackTimeLeft"); }
set { tags.insert ("X-GNOME-BlackTimeLeft", value); }
}
public string? clock_type
{
get { return tags.lookup ("X-GNOME-ClockType"); }
set { tags.insert ("X-GNOME-ClockType", value); }
}
public string? timer_increment
{
get { return tags.lookup ("X-GNOME-TimerIncrement"); }
set { tags.insert ("X-GNOME-TimerIncrement", value); }
}
2010-12-27 01:47:46 +00:00
public bool set_up
{
get { string? v = tags.lookup ("SetUp"); return v != null && v == "1" ? true : false; }
set { tags.insert ("SetUp", value ? "1" : "0"); }
}
public string? fen
{
get { return tags.lookup ("FEN"); }
2010-12-27 02:31:07 +00:00
set { tags.insert ("FEN", value); }
2010-12-27 01:47:46 +00:00
}
public string? termination
{
get { return tags.lookup ("Termination"); }
set { tags.insert ("Termination", value); }
2011-01-09 03:25:33 +00:00
}
public string? white_ai
{
get { return tags.lookup ("X-GNOME-WhiteAI"); }
set { tags.insert ("X-GNOME-WhiteAI", value); }
}
public string? white_level
{
get { return tags.lookup ("X-GNOME-WhiteLevel"); }
set { tags.insert ("X-GNOME-WhiteLevel", value); }
}
public string? black_ai
{
get { return tags.lookup ("X-GNOME-BlackAI"); }
set { tags.insert ("X-GNOME-BlackAI", value); }
}
public string? black_level
{
get { return tags.lookup ("X-GNOME-BlackLevel"); }
set { tags.insert ("X-GNOME-BlackLevel", value); }
}
public PGNGame ()
{
2011-01-01 01:42:50 +00:00
tags = new HashTable<string, string> (str_hash, str_equal);
tags.insert ("Event", "?");
tags.insert ("Site", "?");
tags.insert ("Date", "????.??.??");
tags.insert ("Round", "?");
tags.insert ("White", "?");
tags.insert ("Black", "?");
2011-01-01 03:24:41 +00:00
tags.insert ("Result", PGNGame.RESULT_IN_PROGRESS);
}
public string escape (string value)
{
var a = value.replace ("\\", "\\\\");
return a.replace ("\"", "\\\"");
}
2011-01-01 03:24:41 +00:00
public void write (File file) throws Error
{
2011-01-01 03:24:41 +00:00
var data = new StringBuilder ();
var keys = tags.get_keys ();
2011-01-01 01:42:50 +00:00
keys.sort ((CompareFunc) compare_tag);
foreach (var key in keys)
data.append ("[%s \"%s\"]\n".printf (key, escape (tags.lookup (key))));
2011-01-01 03:24:41 +00:00
data.append ("\n");
int i = 0;
foreach (string move in moves)
{
if (i % 2 == 0)
2011-01-01 03:24:41 +00:00
data.append ("%d. ".printf (i / 2 + 1));
data.append (move);
data.append (" ");
i++;
}
2011-01-01 03:24:41 +00:00
data.append (result);
data.append ("\n");
file.replace_contents (data.str.data, null, false, FileCreateFlags.NONE, null);
}
}
enum State
{
TAGS,
MOVE_TEXT,
LINE_COMMENT,
BRACE_COMMENT,
TAG_START,
TAG_NAME,
PRE_TAG_VALUE,
TAG_VALUE,
POST_TAG_VALUE,
SYMBOL,
PERIOD,
NAG,
ERROR
}
2014-06-24 13:14:59 +00:00
public class PGN : Object
{
2011-01-01 01:42:50 +00:00
public List<PGNGame> games;
private void insert_tag (PGNGame game, string tag_name, string tag_value)
{
switch (tag_name)
{
case "TimeControl":
if (int64.try_parse (tag_value) == true)
game.tags.insert ("TimeControl", tag_value);
else
warning (_("Invalid %s : %s in PGN, setting timer to infinity."), tag_name, tag_value);
break;
case "WhiteTimeLeft":
case "X-GNOME-WhiteTimeLeft":
if (int64.try_parse (tag_value) == false)
warning (_("Invalid %s : %s in PGN, setting timer to infinity."), tag_name, tag_value);
else
game.tags.insert ("X-GNOME-WhiteTimeLeft", tag_value);
break;
case "BlackTimeLeft":
case "X-GNOME-BlackTimeLeft":
if (int64.try_parse (tag_value) == false)
warning (_("Invalid %s : %s in PGN, setting timer to infinity."), tag_name, tag_value);
else
game.tags.insert ("X-GNOME-BlackTimeLeft", tag_value);
break;
case "X-GNOME-ClockType":
2022-08-19 21:18:48 +00:00
if (ChessClockType.string_to_enum (tag_value) == ChessClockType.INVALID)
{
warning (_("Invalid clock type in PGN: %s, using a simple clock."), tag_value);
game.tags.insert ("X-GNOME-ClockType", "simple");
}
break;
case "X-GNOME-TimerIncrement":
if (int64.try_parse (tag_value) == false)
{
warning (_("Invalid timer increment in PGN: %s, using a simple clock."), tag_value);
game.tags["X-GNOME-ClockType"] = "simple";
game.tags.insert ("X-GNOME-TimerIncrement", "0");
}
break;
case "WhiteAI":
case "X-GNOME-WhiteAI":
game.tags.insert ("X-GNOME-WhiteAI", tag_value);
break;
case "WhiteLevel":
case "X-GNOME-WhiteLevel":
game.tags.insert ("X-GNOME-WhiteLevel", tag_value);
break;
case "BlackAI":
case "X-GNOME-BlackAI":
game.tags.insert ("X-GNOME-BlackAI", tag_value);
break;
case "BlackLevel":
case "X-GNOME-BlackLevel":
game.tags.insert ("X-GNOME-BlackLevel", tag_value);
break;
default:
game.tags.insert (tag_name, tag_value);
break;
}
}
public PGN.from_string (string data) throws PGNError
{
// FIXME: Feed through newline at end to make sure parsing complete
State state = State.TAGS, home_state = State.TAGS;
PGNGame game = new PGNGame ();
bool in_escape = false;
size_t token_start = 0, line_offset = 0;
string tag_name = "";
StringBuilder tag_value = new StringBuilder ();
int line = 1;
int rav_level = 0;
for (size_t offset = 0; offset <= data.length; offset++)
{
unichar c = data[(long) offset];
if (c == '\n')
{
line++;
line_offset = offset + 1;
}
switch (state)
{
case State.TAGS:
home_state = State.TAGS;
2015-02-15 03:36:44 +00:00
if (c == ';')
state = State.LINE_COMMENT;
else if (c == '{')
state = State.BRACE_COMMENT;
else if (c == '[')
state = State.TAG_START;
2015-02-15 03:36:44 +00:00
else if (!c.isspace ())
{
offset--;
state = State.MOVE_TEXT;
continue;
}
break;
case State.MOVE_TEXT:
home_state = State.MOVE_TEXT;
2015-02-15 03:36:44 +00:00
if (c == ';')
state = State.LINE_COMMENT;
else if (c == '{')
state = State.BRACE_COMMENT;
else if (c == '*')
{
if (rav_level == 0)
{
2011-01-01 03:24:41 +00:00
game.result = PGNGame.RESULT_IN_PROGRESS;
games.append (game);
game = new PGNGame ();
state = State.TAGS;
}
}
else if (c == '.')
{
offset--;
state = State.PERIOD;
}
else if (c.isalnum ())
{
token_start = offset;
state = State.SYMBOL;
}
else if (c == '$')
{
token_start = offset + 1;
state = State.NAG;
}
else if (c == '(')
{
rav_level++;
continue;
}
else if (c == ')')
{
if (rav_level == 0)
state = State.ERROR;
else
rav_level--;
}
else if (!c.isspace ())
state = State.ERROR;
break;
case State.LINE_COMMENT:
if (c == '\n')
state = home_state;
break;
case State.BRACE_COMMENT:
if (c == '}')
state = home_state;
break;
case State.TAG_START:
if (c.isspace ())
continue;
2015-02-19 15:19:36 +00:00
else if (c.isalnum ())
{
token_start = offset;
state = State.TAG_NAME;
}
else
state = State.ERROR;
break;
case State.TAG_NAME:
if (c.isspace ())
{
2015-02-19 15:19:36 +00:00
tag_name = data[(long) token_start:(long) offset];
state = State.PRE_TAG_VALUE;
}
2015-02-19 15:19:36 +00:00
else if (c.isalnum () || c == '_' || c == '+' || c == '#' || c == '=' || c == ':' || c == '-')
continue;
else
state = State.ERROR;
break;
case State.PRE_TAG_VALUE:
if (c.isspace ())
continue;
else if (c == '"')
{
state = State.TAG_VALUE;
tag_value.erase ();
in_escape = false;
}
else
state = State.ERROR;
break;
case State.TAG_VALUE:
if (c == '\\' && !in_escape)
in_escape = true;
else if (c == '"' && !in_escape)
state = State.POST_TAG_VALUE;
else if (c.isprint ())
{
tag_value.append_unichar (c);
in_escape = false;
}
else
state = State.ERROR;
break;
case State.POST_TAG_VALUE:
if (c.isspace ())
continue;
else if (c == ']')
{
insert_tag (game, tag_name, tag_value.str);
state = State.TAGS;
}
else
state = State.ERROR;
break;
case State.SYMBOL:
/* NOTE: '/' not in spec but required for 1/2-1/2 symbol */
if (c.isalnum () || c == '_' || c == '+' || c == '#' || c == '=' || c == ':' || c == '-' || c == '/')
continue;
else
{
2015-02-19 15:19:36 +00:00
string symbol = data[(long) token_start:(long) offset];
bool is_number = true;
for (int i = 0; i < symbol.length; i++)
if (!symbol[i].isdigit ())
is_number = false;
state = State.MOVE_TEXT;
offset--;
/* Game termination markers */
2011-01-01 03:24:41 +00:00
if (symbol == PGNGame.RESULT_DRAW || symbol == PGNGame.RESULT_WHITE || symbol == PGNGame.RESULT_BLACK)
{
if (rav_level == 0)
{
game.result = symbol;
games.append (game);
game = new PGNGame ();
state = State.TAGS;
}
}
else if (!is_number)
{
if (rav_level == 0)
game.moves.append (symbol);
}
}
break;
case State.PERIOD:
/* FIXME: Should check these move carefully, e.g. "1. e2" */
state = State.MOVE_TEXT;
break;
case State.NAG:
if (c.isdigit ())
continue;
else
{
state = State.MOVE_TEXT;
offset--;
}
break;
case State.ERROR:
size_t char_offset = offset - line_offset - 1;
stderr.printf ("%d.%d: error: Unexpected character\n", line, (int) (char_offset + 1));
2015-02-19 15:19:36 +00:00
stderr.printf ("%s\n", data[(long) line_offset:(long) offset]);
for (int i = 0; i < char_offset; i++)
stderr.printf (" ");
stderr.printf ("^\n");
return;
}
}
2011-01-01 02:42:03 +00:00
if (game.moves.length () > 0 || game.tags.size () > 0)
games.append (game);
2011-01-01 02:42:03 +00:00
/* Must have at least one game */
if (games == null)
2015-02-19 15:19:36 +00:00
throw new PGNError.LOAD_ERROR ("No games in PGN file");
2011-07-01 10:58:45 +00:00
}
public PGN.from_file (File file) throws Error
{
2011-07-01 10:58:45 +00:00
uint8[] contents;
file.load_contents (null, out contents, null);
2011-07-01 10:58:45 +00:00
this.from_string ((string) contents);
}
}