Files
chess-games/pgn-extract/output.c
2024-01-22 07:30:05 +01:00

1798 lines
61 KiB
C

/*
* This file is part of pgn-extract: a Portable Game Notation (PGN) extractor.
* Copyright (C) 1994-2022 David J. Barnes
*
* pgn-extract 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 version.
*
* pgn-extract is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with pgn-extract. If not, see <http://www.gnu.org/licenses/>.
*
* David J. Barnes may be contacted as d.j.barnes@kent.ac.uk
* https://www.cs.kent.ac.uk/people/staff/djb/
*/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <ctype.h>
#include <math.h>
#include <inttypes.h>
#include "bool.h"
#include "defs.h"
#include "typedef.h"
#include "taglist.h"
#include "tokens.h"
#include "lex.h"
#include "grammar.h"
#include "apply.h"
#include "output.h"
#include "mymalloc.h"
/* Functions for outputting games in the required format. */
/* Define the width in which to print a CM move and move number. */
#define MOVE_NUMBER_WIDTH 3
#define MOVE_WIDTH 15
#define CM_COMMENT_CHAR ';'
/* Define the width of the moves area before a comment. */
#define COMMENT_INDENT (MOVE_NUMBER_WIDTH+2+2*MOVE_WIDTH)
/* Define a macro to calculate an array's size. */
#define ARRAY_SIZE(arr) (sizeof(arr)/sizeof(*arr))
/* A small size for formatted move numbers. */
#define FORMATTED_NUMBER_SIZE (20)
/* How much text we have output on the current line. */
static size_t line_length = 0;
/* The buffer in which each output line of a game is built. */
static char *output_line = NULL;
static Boolean print_move(FILE *outputfile, unsigned move_number,
Boolean print_move_number, Boolean white_to_move,
const Move *move_details);
static Boolean
print_items_following_move(FILE *outputfile, const Move *move_details,
unsigned move_number, Boolean white_to_move);
static void output_STR(FILE *outfp, char **Tags);
static void show_tags(FILE *outfp, char **Tags, int tags_length);
static char promoted_piece_letter(Piece piece);
static void print_algebraic_game(Game *current_game, FILE *outputfile,
unsigned move_number, Boolean white_to_move,
Board *final_board);
static void print_EPD_game(Game *current_game, FILE *outputfile,
unsigned move_number, Boolean white_to_move,
Board *final_board);
static void print_FEN_game(Game *current_game, FILE *outputfile,
unsigned move_number, Boolean white_to_move,
Board *final_board);
static void print_EPD_move_list(Game *current_game, FILE *outputfile,
unsigned move_number, Boolean white_to_move,
Board *final_board);
static void print_FEN_move_list(Game *current_game, FILE *outputfile,
unsigned move_number, Boolean white_to_move,
Board *final_board);
static const char *build_FEN_comment(const Board *board);
static void add_hashcode_tag(const Game *game);
static unsigned count_single_move_ply(const Move *move_details, Boolean count_variations);
static unsigned count_move_list_ply(Move *move_list, Boolean count_variations);
static void print_space_separated_str(FILE *outputfile, const char *str);
static void start_comment(FILE *outputfile);
static void end_comment(FILE *outputfile);
static void print_as_comment(FILE *outputfile, const char *str);
static CommentList *create_line_number_comment(const Game *game);
/* List, the order in which the tags should be output.
* The first seven should be the Seven Tag Roster that should
* be present in every game.
* The order of the remainder is, I believe, a matter of taste.
* any PSEUDO_*_TAGs should not appear in this list.
*/
/* Give the array an int type, because a negative value is
* used as a terminator.
*/
static int DefaultTagOrder[] = {
EVENT_TAG, SITE_TAG, DATE_TAG, ROUND_TAG, WHITE_TAG, BLACK_TAG, RESULT_TAG,
#if 1
/* @@@ Consider omitting some of these from the default ordering,
* and allow the output order to be determined from the
* input order.
*/
WHITE_TITLE_TAG, BLACK_TITLE_TAG, WHITE_ELO_TAG, BLACK_ELO_TAG,
WHITE_USCF_TAG, BLACK_USCF_TAG,
WHITE_TYPE_TAG, BLACK_TYPE_TAG,
WHITE_NA_TAG, BLACK_NA_TAG,
ECO_TAG, NIC_TAG, OPENING_TAG, VARIATION_TAG, SUB_VARIATION_TAG,
LONG_ECO_TAG,
TIME_CONTROL_TAG,
ANNOTATOR_TAG,
EVENT_DATE_TAG, EVENT_SPONSOR_TAG, SECTION_TAG, STAGE_TAG, BOARD_TAG,
TIME_TAG, UTC_DATE_TAG, UTC_TIME_TAG,
VARIANT_TAG,
SETUP_TAG,
FEN_TAG,
TERMINATION_TAG, MODE_TAG, PLY_COUNT_TAG,
MATERIAL_MATCH_TAG,
#endif
/* The final value should be negative. */
-1
};
/* Provision for a user-defined tag ordering.
* See add_to_output_tag_order().
* Once allocated, the end of the list must be negative.
*/
static int *TagOrder = NULL;
static int tag_order_space = 0;
void
set_output_line_length(unsigned length)
{
if (output_line != NULL) {
(void) free((void *) output_line);
}
output_line = (char *) malloc_or_die(length + 1);
GlobalState.max_line_length = length;
}
/* Which output format does the user require, based upon the
* given command line argument?
*/
OutputFormat
which_output_format(const char *arg)
{
int i;
struct {
const char *arg;
OutputFormat format;
} formats[] = {
{ "san", SAN},
{ "SAN", SAN},
{ "epd", EPD},
{ "EPD", EPD},
{ "fen", FEN},
{ "FEN", FEN},
{ "lalg", LALG},
{ "halg", HALG},
{ "CM", CM},
{ "LALG", LALG},
{ "HALG", HALG},
{ "ELALG", ELALG},
{ "elalg", ELALG},
{ "XLALG", XLALG},
{ "xlalg", XLALG},
{ "XOLALG", XOLALG},
{ "xolalg", XOLALG},
{ "uci", UCI},
{ "cm", CM},
{ "", SOURCE},
/* Add others before the terminating NULL. */
{ (const char *) NULL, SAN}
};
for (i = 0; formats[i].arg != NULL; i++) {
const char *format_prefix = formats[i].arg;
const size_t format_prefix_len = strlen(format_prefix);
if (strncmp(arg, format_prefix, format_prefix_len) == 0) {
OutputFormat format = formats[i].format;
/* Sanity check. */
if (*format_prefix == '\0' && *arg != '\0') {
fprintf(GlobalState.logfile,
"Unknown output format %s.\n", arg);
exit(1);
}
/* If the format is SAN, it is possible to supply
* a 6-piece suffix listing language-specific
* letters to use in the output.
*/
if ((format == SAN ||
format == ELALG ||
format == XLALG ||
format == XOLALG) &&
(strlen(arg) > format_prefix_len)) {
set_output_piece_characters(&arg[format_prefix_len]);
}
return format;
}
}
fprintf(GlobalState.logfile, "Unknown output format %s.\n", arg);
return SAN;
}
/* Which file suffix should be used for this output format. */
const char *
output_file_suffix(OutputFormat format)
{
/* Define a suffix for the output files. */
static const char PGN_suffix[] = ".pgn";
static const char EPD_suffix[] = ".epd";
static const char FEN_suffix[] = ".fen";
static const char CM_suffix[] = ".cm";
switch (format) {
case SOURCE:
case SAN:
case LALG:
case HALG:
case ELALG:
case XLALG:
case XOLALG:
case UCI:
return PGN_suffix;
case EPD:
return EPD_suffix;
case FEN:
return FEN_suffix;
case CM:
return CM_suffix;
default:
return PGN_suffix;
}
}
static const char *
select_tag_string(TagName tag)
{
const char *tag_string;
if ((tag == PSEUDO_PLAYER_TAG) ||
(tag == PSEUDO_ELO_TAG) ||
(tag == PSEUDO_FEN_PATTERN_TAG) ||
(tag == PSEUDO_FEN_PATTERN_I_TAG)) {
tag_string = NULL;
}
else {
tag_string = tag_header_string(tag);
}
return tag_string;
}
static Boolean
is_STR(TagName tag)
{
switch (tag) {
case EVENT_TAG:
case SITE_TAG:
case DATE_TAG:
case ROUND_TAG:
case WHITE_TAG:
case BLACK_TAG:
case RESULT_TAG:
return TRUE;
default:
return FALSE;
}
}
/* Output the tags held in the Tags structure.
* The full Seven Tag Roster is printed unless
* an element is explicitly suppressed..
*/
static void
output_tag(TagName tag, char **Tags, FILE *outfp)
{
const char *tag_string;
if(is_suppressed_tag(tag)) {
}
else if ((is_STR(tag)) || (Tags[tag] != NULL)) {
/* Must print STR elements and other non-NULL tags. */
tag_string = select_tag_string(tag);
if (tag_string != NULL) {
const char *tag_value;
if (Tags[tag] != NULL) {
tag_value = Tags[tag];
}
else {
if (tag == DATE_TAG) {
tag_value = "????.??.??";
}
else {
tag_value = "?";
}
}
if (GlobalState.json_format) {
fprintf(outfp, "\"%s\" : \"%s\",\n", tag_string, tag_value);
}
else {
fprintf(outfp, "[%s \"%s\"]\n", tag_string, tag_value);
}
}
}
}
/* Output the Seven Tag Roster. */
static void
output_STR(FILE *outfp, char **Tags)
{
unsigned tag_index;
/* Use the default ordering to ensure that STR is output
* in the way it should be.
*/
for (tag_index = 0; tag_index < 7; tag_index++) {
output_tag(DefaultTagOrder[tag_index], Tags, outfp);
}
/* @@@ NB: Strictly speaking, a game with a FEN tag would not be
* meaningful without including it in the output but, historically,
* that has never been done and no one has pointed that out.
* So things have been left as they are until such time as it is
* requested!
*/
}
/* Print out on outfp the current details.
* These can be used in the case of an error.
*/
static void
show_tags(FILE *outfp, char **Tags, int tags_length)
{
int tag_index;
/* Take a copy of the Tags data, so that we can keep
* track of what has been printed. This will make
* it possible to print tags that were identified
* in the source but are not defined with _TAG values.
* See lex.c for how these extra tags are handled.
*/
char **copy_of_tags =
(char **) malloc_or_die(tags_length * sizeof (*copy_of_tags));
int i;
for (i = 0; i < tags_length; i++) {
copy_of_tags[i] = Tags[i];
}
/* Ensure that a tag ordering is available. */
if (TagOrder == NULL) {
/* None set by the user - use the default. */
/* Handle the standard tags.
* The end of the list is marked with a negative value.
*/
for (tag_index = 0; DefaultTagOrder[tag_index] >= 0; tag_index++) {
TagName tag = DefaultTagOrder[tag_index];
output_tag(tag, copy_of_tags, outfp);
copy_of_tags[tag] = (char *) NULL;
}
}
else {
for (tag_index = 0; TagOrder[tag_index] >= 0; tag_index++) {
TagName tag = TagOrder[tag_index];
output_tag(tag, copy_of_tags, outfp);
copy_of_tags[tag] = (char *) NULL;
}
}
/* Handle the remaining tags. */
if(!GlobalState.only_output_wanted_tags) {
for (tag_index = 0; tag_index < tags_length; tag_index++) {
if (copy_of_tags[tag_index] != NULL) {
output_tag(tag_index, copy_of_tags, outfp);
}
}
}
(void) free(copy_of_tags);
putc('\n', outfp);
}
/* Ensure that there is room for len more characters on the
* current line.
*/
static void
check_line_length(FILE *fp, size_t len)
{
if ((line_length + len) > GlobalState.max_line_length) {
terminate_line(fp);
}
}
/* Print ch to fp and update how much of the line
* has been printed on.
*/
static void
print_single_char(FILE *fp, char ch)
{
check_line_length(fp, 1);
output_line[line_length] = ch;
line_length++;
}
/* Print a space, unless at the beginning of a line. */
static void
print_separator(FILE *fp)
{
/* Lines shouldn't have trailing spaces, so ensure that there
* will be room for at least one more character after the space.
*/
check_line_length(fp, 2);
if (line_length != 0 && output_line[line_length - 1] != ' ') {
output_line[line_length] = ' ';
line_length++;
}
}
/* Ensure that what comes next starts on a fresh line. */
void
terminate_line(FILE *fp)
{
/* Delete any trailing space(s). */
while (line_length >= 1 && output_line[line_length - 1] == ' ') {
line_length--;
}
if (line_length > 0) {
output_line[line_length] = '\0';
fprintf(fp, "%s\n", output_line);
line_length = 0;
}
}
/* Print str to fp and update how much of the line
* has been printed on.
*/
void
print_str(FILE *fp, const char *str)
{
size_t len = strlen(str);
check_line_length(fp, len);
if (len > GlobalState.max_line_length) {
fprintf(GlobalState.logfile,
"String length %lu is too long for the line length of %lu:\n",
(unsigned long) len,
(unsigned long) GlobalState.max_line_length);
fprintf(GlobalState.logfile, "%s\n", str);
report_details(GlobalState.logfile);
fprintf(fp, "%s\n", str);
}
else {
sprintf(&(output_line[line_length]), "%s", str);
line_length += len;
}
}
/* Print the given str in separate space-separated
* pieces to take account of line-breaks.
* The str should not contain newline characters.
*/
static void
print_space_separated_str(FILE *fp, const char *str)
{
char *copy = copy_string(str);
const char *chunk = strtok(copy, " ");
while (chunk != NULL) {
print_str(fp, chunk);
chunk = strtok((char *) NULL, " ");
if(chunk != NULL) {
print_separator(fp);
}
}
(void) free((void *) copy);
}
static void
print_comment_list(FILE *fp, CommentList *comment_list)
{
CommentList *next_comment;
for (next_comment = comment_list; next_comment != NULL;
next_comment = next_comment->next) {
StringList *comment = next_comment->comment;
if (comment != NULL) {
start_comment(fp);
for (; comment != NULL; comment = comment->next) {
print_space_separated_str(fp, comment->str);
if(comment->next != NULL) {
print_separator(fp);
}
}
end_comment(fp);
}
}
}
static void
print_move_list(FILE *outputfile, unsigned move_number, Boolean white_to_move,
const Move *move_details, const Board *final_board)
{
Boolean print_move_number = TRUE;
const Move *move = move_details;
Boolean keepPrinting;
int plies;
/* Keep track of the number of consecutive quiescent moves:
* captures, Checks and promotion are non-quiescent.
* @@@ NB: This does not strictly apply to just the main line
* and could trigger quiescence in a variation, which is undesirable.
*/
unsigned quiescense_count = 0;
/* Work out the ply depth. */
plies = 2 * (move_number) - 1;
if (!white_to_move) {
plies++;
}
if (GlobalState.output_ply_limit >= 0 &&
plies > GlobalState.output_ply_limit) {
keepPrinting = FALSE;
}
else {
keepPrinting = TRUE;
}
while (move != NULL && keepPrinting) {
if (GlobalState.json_format) {
fputs("{ ", outputfile);
}
/* Reset print_move number if a variation was printed. */
print_move_number = print_move(outputfile, move_number,
print_move_number,
white_to_move, move);
/* See if there is a result attached. This may be attached either
* to a move or a comment.
*/
if (!GlobalState.check_only && (move != NULL) &&
(move->terminating_result != NULL)) {
if (GlobalState.output_FEN_string &&
GlobalState.FEN_comment_pattern == NULL &&
final_board != NULL) {
if(GlobalState.json_format) {
if(!GlobalState.add_FEN_comments) {
char *fen = get_FEN_string(final_board);
fprintf(outputfile, ", \"FEN\" : \"%s\" ", fen);
(void) free((void *) fen);
}
else {
/* The final FEN position will have been output anyway. */
}
}
else {
print_separator(outputfile);
const char *comment = build_FEN_comment(final_board);
print_str(outputfile, comment);
(void) free((void *) comment);
}
}
if (GlobalState.keep_results) {
print_separator(outputfile);
print_str(outputfile, move->terminating_result);
}
}
if (move->move[0] != '\0') {
/* A genuine move was just printed, rather than a comment. */
if (white_to_move) {
white_to_move = FALSE;
}
else {
move_number++;
white_to_move = TRUE;
}
plies++;
if (move->captured_piece != EMPTY ||
move->check_status != NOCHECK ||
move->promoted_piece != EMPTY) {
quiescense_count = 0;
}
else {
quiescense_count++;
}
if (GlobalState.output_ply_limit >= 0 &&
plies > GlobalState.output_ply_limit &&
quiescense_count >= GlobalState.quiescence_threshold) {
keepPrinting = FALSE;
}
}
if (GlobalState.json_format) {
fputs(" }", outputfile);
}
move = move->next;
/* The following is slightly inaccurate.
* If the previous value of move was a comment and
* we aren't printing comments, then this results in two
* separators being printed after the move preceding the comment.
* Not sure how to cleanly fix it, because there might have
* been nags attached to the comment that were printed, for instance!
*/
if (move != NULL && keepPrinting) {
if (GlobalState.json_format) {
fputs(", ", outputfile);
}
else {
print_separator(outputfile);
}
}
}
if (move != NULL && !keepPrinting) {
/* We ended printing the game prematurely.
*
* Decide whether to print a result indicator.
*/
if (GlobalState.keep_results) {
/* Find the final move to see if there was a result there. */
while (move->next != NULL) {
move = move->next;
}
if (move->terminating_result != NULL) {
print_separator(outputfile);
print_str(outputfile, "*");
}
}
}
}
/* Output the current move along with associated information.
* Return TRUE if either a variation or comment was printed,
* FALSE otherwise.
* This is needed to determine whether a new move number
* is to be printed after a variation.
*/
/* A length to accommodate move numbers. */
#define SMALL_MOVE_NUMBER_LENGTH (20)
static Boolean
print_move(FILE *outputfile, unsigned move_number, Boolean print_move_number,
Boolean white_to_move, const Move *move_details)
{
Boolean something_printed = FALSE;
OutputFormat output_format = GlobalState.output_format;
if (move_details == NULL) {
/* Shouldn't happen. */
fprintf(GlobalState.logfile,
"Internal error: NULL move in print_move.\n");
report_details(GlobalState.logfile);
}
else {
if (GlobalState.check_only) {
/* Nothing to be output. */
}
else {
const unsigned char *move_text = move_details->move;
/* What move text to print. */
char *move_to_print;
if (*move_text != '\0') {
if (GlobalState.keep_move_numbers &&
(white_to_move || print_move_number)) {
static char small_number[SMALL_MOVE_NUMBER_LENGTH];
/* @@@ Should 1... be written as 1. ... ? */
sprintf(small_number,
"%u.%s", move_number,
white_to_move ? "" : "..");
print_str(outputfile, small_number);
print_separator(outputfile);
}
switch (output_format) {
case SAN:
case SOURCE:
/* @@@ move_text should be handled as unsigned
* char text, as the source may be 8-bit rather
* than 7-bit.
*/
move_to_print = copy_string((const char *) move_text);
if (!GlobalState.keep_checks) {
/* Look for a check or mate symbol. */
char *check = strchr((const char *) move_text, '+');
if (check == NULL) {
check = strchr((const char *) move_text, '#');
}
if (check != NULL) {
/* We need to drop it from move_text. */
int len = check - ((char *) move_text);
move_to_print[len] = '\0';
}
}
break;
case HALG:
{
char algebraic[MAX_MOVE_LEN + 1];
*algebraic = '\0';
switch (move_details->class) {
case PAWN_MOVE:
case ENPASSANT_PAWN_MOVE:
case KINGSIDE_CASTLE:
case QUEENSIDE_CASTLE:
case PIECE_MOVE:
sprintf(algebraic,
"%c%c-%c%c",
move_details->from_col,
move_details->from_rank,
move_details->to_col,
move_details->to_rank);
break;
case PAWN_MOVE_WITH_PROMOTION:
sprintf(algebraic,
"%c%c-%c%c%c",
move_details->from_col,
move_details->from_rank,
move_details->to_col,
move_details->to_rank,
promoted_piece_letter(move_details->promoted_piece));
break;
case NULL_MOVE:
strcpy(algebraic, NULL_MOVE_STRING);
break;
case UNKNOWN_MOVE:
strcpy(algebraic, "???");
break;
}
if (GlobalState.keep_checks) {
switch (move_details->check_status) {
case NOCHECK:
break;
case CHECK:
strcat(algebraic, "+");
break;
case CHECKMATE:
strcat(algebraic, "#");
break;
}
}
move_to_print = copy_string(algebraic);
}
break;
case LALG:
case ELALG:
case XLALG:
case XOLALG:
case UCI:
{
char algebraic[MAX_MOVE_LEN + 1];
size_t ind = 0;
if(output_format == XOLALG &&
(move_details->class == KINGSIDE_CASTLE ||
move_details->class == QUEENSIDE_CASTLE)) {
strcpy(algebraic, (char *) move_text);
ind += strlen((char *) algebraic);
}
else {
/* Prefix with a piece name if ELALG. */
if ((output_format == ELALG ||
output_format == XLALG ||
output_format == XOLALG) &&
move_details->class == PIECE_MOVE) {
strcpy(algebraic,
piece_str(move_details->piece_to_move));
ind = strlen(algebraic);
}
/* Format the basics. */
if (move_details->class != NULL_MOVE) {
sprintf(&algebraic[ind],
"%c%c",
move_details->from_col,
move_details->from_rank);
ind += 2;
if (output_format == XLALG ||
output_format == XOLALG) {
/* Add a separating - or x. */
char separator;
if (move_details->captured_piece != EMPTY) {
separator = 'x';
}
else {
separator = '-';
}
sprintf(&algebraic[ind],
"%c", separator);
ind++;
}
sprintf(&algebraic[ind],
"%c%c",
move_details->to_col,
move_details->to_rank);
ind += 2;
}
else {
strcpy(algebraic, NULL_MOVE_STRING);
ind += strlen(NULL_MOVE_STRING);
}
switch (move_details->class) {
case PAWN_MOVE:
case KINGSIDE_CASTLE:
case QUEENSIDE_CASTLE:
case PIECE_MOVE:
case NULL_MOVE:
/* Nothing more to do at this stage. */
break;
case ENPASSANT_PAWN_MOVE:
if (output_format == ELALG ||
output_format == XLALG ||
output_format == XOLALG) {
strcat(algebraic, "ep");
ind += 2;
}
break;
case PAWN_MOVE_WITH_PROMOTION:
sprintf(&algebraic[ind],
"%s",
piece_str(move_details->promoted_piece));
ind = strlen(algebraic);
break;
case UNKNOWN_MOVE:
strcpy(algebraic, "???");
ind += 3;
break;
}
}
if (GlobalState.keep_checks) {
switch (move_details->check_status) {
case NOCHECK:
break;
case CHECK:
strcat(algebraic, "+");
ind++;
break;
case CHECKMATE:
strcat(algebraic, "#");
ind++;
break;
}
}
move_to_print = copy_string(algebraic);
}
break;
default:
fprintf(GlobalState.logfile,
"Unknown output format %d in print_move()\n",
output_format);
exit(1);
move_to_print = NULL;
break;
}
}
else {
/* An empty move. */
fprintf(GlobalState.logfile,
"Internal error: Empty move in print_move.\n");
report_details(GlobalState.logfile);
move_to_print = NULL;
}
if (GlobalState.json_format) {
fputs("\"move\" : ", outputfile);
fputs("\"", outputfile);
if (move_to_print != NULL) {
fputs(move_to_print, outputfile);
}
fputs("\"", outputfile);
}
else {
if (move_to_print != NULL) {
print_str(outputfile, move_to_print);
}
}
if (move_to_print != NULL) {
(void) free(move_to_print);
}
if(print_items_following_move(outputfile, move_details, move_number, white_to_move)) {
something_printed = TRUE;
}
}
}
return something_printed;
}
/* Maximum length of a 64-bit unsigned int in decimal.
* NB: At the moment, the output is hex, which requires
* only 16 characters.
*/
#define HASH_64_BIT_SPACE 20
/* Print further information, that may be attached to moves,
* such as NAGs and comments.
* Return TRUE if something was printed for non JSON format output.
*/
static Boolean
print_items_following_move(FILE *outputfile, const Move *move_details,
unsigned move_number, Boolean white_to_move)
{
Boolean something_printed = FALSE;
Nag *nags = move_details->NAGs;
Variation *variants = move_details->Variants;
if (move_details->comment_list != NULL && GlobalState.keep_comments) {
print_comment_list(outputfile, move_details->comment_list);
something_printed = TRUE;
}
if(nags != NULL) {
/* We don't need to output move numbers after just
* NAGs, so don't set something_printed just for NAGs.
*/
if (GlobalState.keep_NAGs && GlobalState.json_format) {
fputs(", \"nags\" : [", outputfile);
}
while (nags != NULL) {
if(GlobalState.keep_NAGs) {
StringList *text = nags->text;
while(text != NULL) {
if(GlobalState.json_format) {
fprintf(outputfile, "\"%s\"", text->str);
if(nags->next != NULL) {
fputs(", ", outputfile);
}
}
else {
print_separator(outputfile);
print_str(outputfile, text->str);
}
text = text->next;
}
}
if(nags->comments != NULL && GlobalState.keep_comments) {
// @@@ JSON option needed.
print_comment_list(outputfile, nags->comments);
something_printed = TRUE;
}
nags = nags->next;
}
if(GlobalState.keep_NAGs && GlobalState.json_format) {
fputs("] ", outputfile);
}
}
if (GlobalState.output_evaluation) {
if(GlobalState.json_format) {
fprintf(outputfile, ", \"evaluation\" : \"%.2f\"",
move_details->evaluation);
}
else {
const char valueSpace[] = "-012456789.00";
char *evaluation = (char *) malloc_or_die(sizeof (valueSpace));
sprintf(evaluation, "%.2f", move_details->evaluation);
if (strlen(evaluation) > strlen(valueSpace)) {
fprintf(GlobalState.logfile,
"Internal error: Overflow in evaluation space in print_items_following_move()\n");
exit(1);
}
print_as_comment(outputfile, evaluation);
(void) free((void *) evaluation);
something_printed = TRUE;
}
}
if(GlobalState.add_FEN_comments) {
if(move_details->epd != NULL && move_details->fen_suffix != NULL) {
if(GlobalState.json_format) {
fprintf(outputfile, ", \"FEN\" : \"%s %s\"",
move_details->epd,
move_details->fen_suffix);
}
else {
start_comment(outputfile);
print_space_separated_str(outputfile, move_details->epd);
print_separator(outputfile);
print_space_separated_str(outputfile, move_details->fen_suffix);
end_comment(outputfile);
something_printed = TRUE;
}
}
}
if (GlobalState.add_hashcode_comments) {
if(GlobalState.json_format) {
fprintf(outputfile, ", \"HashCode\" : \"");
fprintf(outputfile, "%016" PRIx64, move_details->zobrist);
fprintf(outputfile, "\"");
}
else {
char *hashcode = (char *) malloc_or_die(HASH_64_BIT_SPACE + 1);
sprintf(hashcode, "%016" PRIx64, move_details->zobrist);
print_as_comment(outputfile, hashcode);
(void) free((void *) hashcode);
something_printed = TRUE;
}
}
if (variants != NULL) {
if(GlobalState.keep_variations) {
if(!GlobalState.split_variants) {
while (variants != NULL) {
print_separator(outputfile);
print_single_char(outputfile, '(');
if (GlobalState.keep_comments &&
(variants->prefix_comment != NULL)) {
print_comment_list(outputfile, variants->prefix_comment);
print_separator(outputfile);
}
/* Always start with a move number.
* The final board position is not needed.
*/
print_move_list(outputfile, move_number,
white_to_move, variants->moves,
(const Board *) NULL);
print_single_char(outputfile, ')');
if (GlobalState.keep_comments &&
(variants->suffix_comment != NULL)) {
print_comment_list(outputfile, variants->suffix_comment);
}
variants = variants->next;
}
something_printed = TRUE;
}
else {
/* Variations are being split so don't output them. */
}
}
else if(GlobalState.keep_comments) {
/* Avoid losing suffix comments of variations.
* NB: @@@ I am not entirely convinced that comments that
* follow variations actually belong with the move immediately
* preceding the variation. However, this is one way to avoid
* their being lost if variations are not being output.
*/
while(variants != NULL) {
if(variants->suffix_comment != NULL) {
print_comment_list(outputfile, variants->suffix_comment);
something_printed = TRUE;
}
variants = variants->next;
}
}
}
return something_printed;
}
/* Start a comment, taking into account separators
* and possible placement of comments on separate lines.
*/
static void
start_comment(FILE *outputfile)
{
if(GlobalState.separate_comment_lines) {
terminate_line(outputfile);
}
else {
print_separator(outputfile);
}
print_single_char(outputfile, '{');
print_separator(outputfile);
}
/* End a comment, taking into account separators
* and possible placement of comments on separate lines.
*/
static void
end_comment(FILE *outputfile)
{
print_separator(outputfile);
print_single_char(outputfile, '}');
if(GlobalState.separate_comment_lines) {
terminate_line(outputfile);
}
}
/* Print str as a comment. */
static void
print_as_comment(FILE *outputfile, const char *str)
{
start_comment(outputfile);
print_str(outputfile, str);
end_comment(outputfile);
}
/* Return the letter associated with the given piece. */
static char
promoted_piece_letter(Piece piece)
{
switch (piece) {
case QUEEN:
return 'Q';
case ROOK:
return 'R';
case BISHOP:
return 'B';
case KNIGHT:
return 'N';
default:
return '?';
}
}
/* Output a comment in CM format. */
static void
output_cm_comment(CommentList *comment, FILE *outputfile, unsigned indent)
{ /* Don't indent for the first comment line, because
* we should already be positioned at the correct spot.
*/
unsigned indent_for_this_line = 0;
putc(CM_COMMENT_CHAR, outputfile);
line_length++;
while (comment != NULL) {
/* We will use strtok to break up the comment string,
* with chunk to point to each bit in turn.
*/
char *chunk;
StringList *comment_str = comment->comment;
for (; comment_str != NULL; comment_str = comment_str->next) {
char *str = copy_string(comment_str->str);
chunk = strtok(str, " ");
while (chunk != NULL) {
size_t len = strlen(chunk);
if ((line_length + 1 + len) > GlobalState.max_line_length) {
/* Start a new line. */
fputc('\n', outputfile);
indent_for_this_line = indent;
for (unsigned in = 0; in < indent_for_this_line; in++) {
fputc(' ', outputfile);
}
fputc(CM_COMMENT_CHAR, outputfile);
fputc(' ', outputfile);
line_length = indent_for_this_line + 2;
}
else {
fputc(' ', outputfile);
line_length++;
}
fprintf(outputfile, "%s", chunk);
line_length += len;
chunk = strtok((char *) NULL, " ");
}
(void) free((void *) str);
}
comment = comment->next;
}
fputc('\n', outputfile);
line_length = 0;
}
static void
output_cm_result(const char *result, FILE *outputfile)
{
fprintf(outputfile, "%c ", CM_COMMENT_CHAR);
if (strcmp(result, "1-0") == 0) {
fprintf(outputfile, "and black resigns");
}
else if (strcmp(result, "0-1") == 0) {
fprintf(outputfile, "and white resigns");
}
else if (strncmp(result, "1/2", 3) == 0) {
fprintf(outputfile, "draw");
}
else {
fprintf(outputfile, "incomplete result");
}
}
/* Output the game in Chess Master format.
* This is probably obsolete.
*/
static void
output_cm_game(FILE *outputfile, unsigned move_number,
Boolean white_to_move, const Game *game)
{
const Move *move = game->moves;
if ((move_number != 1) || (!white_to_move)) {
fprintf(GlobalState.logfile,
"Unable to output CM games other than from the starting position.\n");
report_details(GlobalState.logfile);
}
fprintf(outputfile, "WHITE: %s\n",
game->tags[WHITE_TAG] != NULL ? game->tags[WHITE_TAG] : "");
fprintf(outputfile, "BLACK: %s\n",
game->tags[BLACK_TAG] != NULL ? game->tags[BLACK_TAG] : "");
putc('\n', outputfile);
if (game->prefix_comment != NULL) {
line_length = 0;
output_cm_comment(game->prefix_comment, outputfile, 0);
}
while (move != NULL) {
if (move->move[0] != '\0') {
/* A genuine move. */
if (white_to_move) {
fprintf(outputfile, "%*u. ", MOVE_NUMBER_WIDTH, move_number);
fprintf(outputfile, "%*s", -MOVE_WIDTH, move->move);
white_to_move = FALSE;
}
else {
fprintf(outputfile, "%*s", -MOVE_WIDTH, move->move);
move_number++;
white_to_move = TRUE;
}
}
if ((move->comment_list != NULL) && GlobalState.keep_comments) {
const char *result = move->terminating_result;
if (!white_to_move) {
fprintf(outputfile, "%*s", -MOVE_WIDTH, "...");
}
line_length = COMMENT_INDENT;
output_cm_comment(move->comment_list, outputfile, COMMENT_INDENT);
if ((result != NULL) && (move->check_status != CHECKMATE)) {
/* Give some information on the nature of the finish. */
if (white_to_move) {
fprintf(outputfile, "%*s", COMMENT_INDENT, "");
}
else {
/* Print out a string representing the result. */
fprintf(outputfile, "%*s %*s%*s",
MOVE_NUMBER_WIDTH + 1, "", -MOVE_WIDTH, "...",
MOVE_WIDTH, "");
}
output_cm_result(result, outputfile);
putc('\n', outputfile);
}
else {
if (!white_to_move) {
/* Indicate that the next move is Black's. */
fprintf(outputfile, "%*s %*s",
MOVE_NUMBER_WIDTH + 1, "", -MOVE_WIDTH, "...");
}
}
}
else {
if ((move->terminating_result != NULL) &&
(move->check_status != CHECKMATE)) {
/* Give some information on the nature of the finish. */
const char *result = move->terminating_result;
if (!white_to_move) {
fprintf(outputfile, "%*s", -MOVE_WIDTH, "...");
}
output_cm_result(result, outputfile);
if (!white_to_move) {
putc('\n', outputfile);
fprintf(outputfile, "%*s %*s",
MOVE_NUMBER_WIDTH + 1, "", -MOVE_WIDTH, "...");
}
putc('\n', outputfile);
}
else {
if (white_to_move) {
/* Terminate the move pair. */
putc('\n', outputfile);
}
}
}
move = move->next;
}
putc('\n', outputfile);
}
/* Output the current game according to the required output format. */
void
format_game(Game *current_game, FILE *outputfile)
{
Boolean white_to_move = TRUE;
unsigned move_number = 1;
Board *initial_board;
/* The final board position, if available. */
Board *final_board = NULL;
if(GlobalState.line_number_marker != NULL) {
CommentList *comment = create_line_number_comment(current_game);
comment->next = current_game->prefix_comment;
current_game->prefix_comment = comment;
}
/* We need a copy of the final board.
* Combine the generation of this with a rewrite
* of the moves of the game into
* SAN (Standard Algebraic Notation) unless the original
* source form is required.
*/
final_board = rewrite_game(current_game);
initial_board = new_game_board(current_game->tags[FEN_TAG]);
/* If we aren't starting from the initial setup, then we
* need to know the current move number and whose
* move it is.
*/
if (current_game->tags[FEN_TAG] != NULL && initial_board != NULL) {
move_number = initial_board->move_number;
white_to_move = initial_board->to_move == WHITE;
}
/* Start at the beginning of a line. */
line_length = 0;
if (final_board != NULL) {
if (GlobalState.output_plycount) {
add_plycount(current_game);
}
if (GlobalState.output_total_plycount) {
add_total_plycount(current_game, GlobalState.keep_variations && !GlobalState.split_variants);
}
if (GlobalState.add_hashcode_tag || current_game->tags[HASHCODE_TAG] != NULL) {
add_hashcode_tag(current_game);
}
switch (GlobalState.output_format) {
case SAN:
case SOURCE:
case LALG:
case HALG:
case ELALG:
case XLALG:
case XOLALG:
case UCI:
print_algebraic_game(current_game, outputfile, move_number, white_to_move,
final_board);
break;
case EPD:
print_EPD_game(current_game, outputfile, move_number, white_to_move,
initial_board);
break;
case FEN:
print_FEN_game(current_game, outputfile, move_number, white_to_move,
initial_board);
break;
case CM:
output_cm_game(outputfile, move_number, white_to_move, current_game);
break;
default:
fprintf(GlobalState.logfile,
"Internal error: unknown output type %d in format_game().\n",
GlobalState.output_format);
break;
}
fflush(outputfile);
free_board(final_board);
}
free_board(initial_board);
}
/* Add the given tag to the output ordering. */
void
add_to_output_tag_order(TagName tag)
{
int tag_index;
if (TagOrder == NULL) {
tag_order_space = ARRAY_SIZE(DefaultTagOrder);
TagOrder = (int *) malloc_or_die(tag_order_space * sizeof (*TagOrder));
/* Always ensure that there is a negative value at the end. */
TagOrder[0] = -1;
}
/* Check to ensure a position has not already been indicated
* for this tag.
*/
for (tag_index = 0; (TagOrder[tag_index] != -1) &&
(TagOrder[tag_index] != (int) tag); tag_index++) {
}
if (TagOrder[tag_index] == -1) {
/* Make sure there is enough space for another. */
if (tag_index >= tag_order_space) {
/* Allocate some more. */
tag_order_space += 10;
TagOrder = (int *) realloc_or_die((void *) TagOrder,
tag_order_space * sizeof (*TagOrder));
}
TagOrder[tag_index] = tag;
TagOrder[tag_index + 1] = -1;
}
else {
fprintf(GlobalState.logfile, "Duplicate position for tag: %s\n",
select_tag_string(tag));
}
}
/* Format EPD comments containing tag details.
* A c0 comment contains player and event info.
* A c1 comment contains the game result.
*/
static const char *
format_epd_game_comment(char **Tags)
{
static char c0_prefix[] = "c0 ";
static char c1_prefix[] = "c1 ";
static char player_separator[] = "-";
static size_t prefix_and_separator_len =
sizeof (c0_prefix) + sizeof (player_separator);
size_t space_needed = prefix_and_separator_len;
char *comment;
if (Tags[WHITE_TAG] != NULL) {
space_needed += strlen(Tags[WHITE_TAG]);
}
if (Tags[BLACK_TAG] != NULL) {
space_needed += strlen(Tags[BLACK_TAG]);
}
/* Allow a space character before each of the remaining tags. */
if (Tags[EVENT_TAG] != NULL) {
space_needed += 1 + strlen(Tags[EVENT_TAG]);
}
if (Tags[SITE_TAG] != NULL) {
space_needed += 1 + strlen(Tags[SITE_TAG]);
}
if (Tags[DATE_TAG] != NULL) {
space_needed += 1 + strlen(Tags[DATE_TAG]);
}
/* Allow for a terminating semicolon after the c0 comment. */
space_needed++;
/* Allow for a space before the c1 comment. */
space_needed++;
space_needed += strlen(c1_prefix);
if(Tags[RESULT_TAG] != NULL) {
space_needed += 1 + strlen(Tags[RESULT_TAG]);
}
else {
space_needed += 1 + strlen("*");
}
/* Allow for a terminating semicolon after the c1 comment. */
space_needed++;
comment = (char *) malloc_or_die(space_needed + 1);
strcpy(comment, c0_prefix);
if (Tags[WHITE_TAG] != NULL) {
strcat(comment, Tags[WHITE_TAG]);
}
strcat(comment, player_separator);
if (Tags[BLACK_TAG] != NULL) {
strcat(comment, Tags[BLACK_TAG]);
}
if (Tags[EVENT_TAG] != NULL) {
strcat(comment, " ");
strcat(comment, Tags[EVENT_TAG]);
}
if (Tags[SITE_TAG] != NULL) {
strcat(comment, " ");
strcat(comment, Tags[SITE_TAG]);
}
if (Tags[DATE_TAG] != NULL) {
strcat(comment, " ");
strcat(comment, Tags[DATE_TAG]);
}
strcat(comment, "; ");
strcat(comment, c1_prefix);
if(Tags[RESULT_TAG] != NULL) {
strcat(comment, Tags[RESULT_TAG]);
}
else {
strcat(comment, "*");
}
strcat(comment, ";");
if (strlen(comment) >= space_needed) {
fprintf(GlobalState.logfile,
"Internal error: overflow in format_epd_game_comment\n");
}
return comment;
}
static void
print_algebraic_game(Game *current_game, FILE *outputfile,
unsigned move_number, Boolean white_to_move,
Board *final_board)
{
if (GlobalState.json_format) {
/* Need to take account of splitting output over multiple files. */
Boolean comma_needed;
if (GlobalState.games_per_file == 0) {
comma_needed = GlobalState.num_games_matched > 1;
}
else {
comma_needed = GlobalState.games_per_file > 1 &&
(GlobalState.num_games_matched % GlobalState.games_per_file) != 1;
}
if (comma_needed) {
fputs(",\n", outputfile);
}
fputs("{\n", outputfile);
}
/* Report details on the output. */
if (GlobalState.tag_output_format == ALL_TAGS) {
show_tags(outputfile, current_game->tags, current_game->tags_length);
}
else if (GlobalState.tag_output_format == SEVEN_TAG_ROSTER) {
output_STR(outputfile, current_game->tags);
if (GlobalState.add_ECO && !GlobalState.parsing_ECO_file) {
/* If ECO classification has been requested, then assume
* that ECO tags are also required.
*/
output_tag(ECO_TAG, current_game->tags, outputfile);
output_tag(OPENING_TAG, current_game->tags, outputfile);
output_tag(VARIATION_TAG, current_game->tags, outputfile);
output_tag(SUB_VARIATION_TAG, current_game->tags, outputfile);
}
if(current_game->tags[FEN_TAG] != NULL) {
/* Output any FEN that there might be. */
/* @@@ NB: Strictly speaking, we should check TagOrder for the
* preferred order of these, in case it is not the default.
*/
output_tag(VARIANT_TAG, current_game->tags, outputfile);
output_tag(SETUP_TAG, current_game->tags, outputfile);
output_tag(FEN_TAG, current_game->tags, outputfile);
}
putc('\n', outputfile);
}
else if (GlobalState.tag_output_format == NO_TAGS) {
}
else {
fprintf(GlobalState.logfile,
"Unknown output form for tags: %d\n",
GlobalState.tag_output_format);
exit(1);
}
if ((GlobalState.keep_comments) &&
(current_game->prefix_comment != NULL)) {
print_comment_list(outputfile,
current_game->prefix_comment);
terminate_line(outputfile);
putc('\n', outputfile);
}
if (GlobalState.json_format) {
fputs("\"Moves\":[", outputfile);
}
print_move_list(outputfile, move_number, white_to_move,
current_game->moves, final_board);
if (GlobalState.json_format) {
fputs("]\n", outputfile);
}
/* Take account of a possible zero move game. */
if (current_game->moves == NULL) {
if (current_game->tags[RESULT_TAG] != NULL) {
if (!GlobalState.json_format) {
print_str(outputfile, current_game->tags[RESULT_TAG]);
}
}
else {
fprintf(GlobalState.logfile,
"Internal error: Zero move game with no result\n");
}
}
if (GlobalState.json_format) {
fputs("\n}", outputfile);
}
else {
terminate_line(outputfile);
putc('\n', outputfile);
}
}
static void
print_EPD_move_list(Game *current_game, FILE *outputfile,
unsigned move_number, Boolean white_to_move,
Board *initial_board)
{
const char *game_comment = format_epd_game_comment(current_game->tags);
const Move *move = current_game->moves;
if (initial_board != NULL) {
char epd[FEN_SPACE];
build_basic_EPD_string(initial_board, epd);
fprintf(outputfile, "%s %s\n", epd, game_comment);
}
while (move != NULL) {
if (move->epd != NULL) {
fprintf(outputfile, "%s %s\n", move->epd, game_comment);
}
else {
fprintf(GlobalState.logfile, "Internal error: Missing EPD\n");
report_details(GlobalState.logfile);
exit(1);
}
move = move->next;
}
(void) free((void *) game_comment);
}
static void print_FEN_move_list(Game *current_game, FILE *outputfile,
unsigned move_number, Boolean white_to_move,
Board *initial_board)
{
Board *board = initial_board;
Move *move = current_game->moves;
Boolean keepPrinting;
/* Work out the ply depth. */
int plies = 2 * (move_number) - 1;
if (!white_to_move) {
plies++;
}
if (GlobalState.output_ply_limit >= 0 &&
plies > GlobalState.output_ply_limit) {
keepPrinting = FALSE;
}
else {
keepPrinting = TRUE;
const char *FEN_string = get_FEN_string(board);
fprintf(GlobalState.outputfile, "%s\n", FEN_string);
free((void *) FEN_string);
}
while (move != NULL && keepPrinting) {
if (move->move[0] != '\0') {
if(apply_move(move, board)) {
const char *FEN_string = get_FEN_string(board);
fprintf(GlobalState.outputfile, "%s\n", FEN_string);
free((void *) FEN_string);
}
else {
keepPrinting = FALSE;
}
/* A genuine move was just printed, rather than a comment. */
if (white_to_move) {
white_to_move = FALSE;
}
else {
move_number++;
white_to_move = TRUE;
}
plies++;
}
move = move->next;
}
}
static void
print_EPD_game(Game *current_game, FILE *outputfile,
unsigned move_number, Boolean white_to_move,
Board *initial_board)
{
if (!GlobalState.check_only) {
print_EPD_move_list(current_game, outputfile, move_number, white_to_move,
initial_board);
putc('\n', outputfile);
}
}
static void
print_FEN_game(Game *current_game, FILE *outputfile,
unsigned move_number, Boolean white_to_move,
Board *initial_board)
{
if (!GlobalState.check_only) {
/* Report details on the output. */
if (GlobalState.tag_output_format == ALL_TAGS) {
show_tags(outputfile, current_game->tags, current_game->tags_length);
}
else if (GlobalState.tag_output_format == SEVEN_TAG_ROSTER) {
output_STR(outputfile, current_game->tags);
}
else if (GlobalState.tag_output_format == NO_TAGS) {
}
else {
fprintf(GlobalState.logfile,
"Unknown output form for tags: %d\n",
GlobalState.tag_output_format);
exit(1);
}
print_FEN_move_list(current_game, outputfile, move_number, white_to_move,
initial_board);
putc('\n', outputfile);
}
}
/*
* Build a comment containing a FEN representation of the board.
*/
static const char *
build_FEN_comment(const Board *board)
{
char *fen = get_FEN_string(board);
const char *prefix = "{ \"";
const char *suffix = "\" }";
char *comment = (char *) malloc_or_die(strlen(prefix) + strlen(fen) + strlen(suffix) + 1);
sprintf(comment, "%s%s%s", prefix, fen, suffix);
(void) free((void *) fen);
return comment;
}
/*
* Count how many ply recorded for the given move.
* Include variations if count_variations.
*/
static unsigned count_single_move_ply(const Move *move_details,
Boolean count_variations)
{
unsigned count = 1;
if (count_variations) {
Variation *variant = move_details->Variants;
while (variant != NULL) {
count += count_move_list_ply(variant->moves, count_variations);
variant = variant->next;
}
}
return count;
}
/*
* Count how many plies in the game in total.
* Include variations if count_variations.
*/
static unsigned count_move_list_ply(Move *move_list, Boolean count_variations)
{
unsigned count = 0;
while (move_list != NULL) {
count += count_single_move_ply(move_list, count_variations);
move_list = move_list->next;
}
return count;
}
/*
* Count how many plies in the game in total.
* Include variations if count_variations.
*/
void add_plycount(const Game *game)
{
unsigned count = count_move_list_ply(game->moves, FALSE);
char formatted_count[FORMATTED_NUMBER_SIZE];
sprintf(formatted_count, "%u", count);
if (game->tags[PLY_COUNT_TAG] != NULL) {
(void) free(game->tags[PLY_COUNT_TAG]);
}
game->tags[PLY_COUNT_TAG] = copy_string(formatted_count);
}
/*
* Count how many plies in the game in total.
* Include variations if count_variations.
*/
void add_total_plycount(const Game *game, Boolean count_variations)
{
unsigned count = count_move_list_ply(game->moves, count_variations);
char formatted_count[FORMATTED_NUMBER_SIZE];
sprintf(formatted_count, "%u", count);
if (game->tags[TOTAL_PLY_COUNT_TAG] != NULL) {
(void) free(game->tags[TOTAL_PLY_COUNT_TAG]);
}
game->tags[TOTAL_PLY_COUNT_TAG] = copy_string(formatted_count);
}
/*
* Include a tag containing a hashcode for the game.
* Use the cumulative hash value.
*/
static void add_hashcode_tag(const Game *game)
{
HashCode hashcode = game->cumulative_hash_value;
char formatted_code[FORMATTED_NUMBER_SIZE];
sprintf(formatted_code, "%08x", (unsigned) hashcode);
if (game->tags[HASHCODE_TAG] != NULL) {
(void) free(game->tags[HASHCODE_TAG]);
}
game->tags[HASHCODE_TAG] = copy_string(formatted_code);
}
/* Determine how many characters needed to format the given number.
* Required for accurate space allocation.
*/
static unsigned lineNumberChars(unsigned long num)
{
return (unsigned)((ceil(log10(num))+1));
}
static CommentList *create_line_number_comment(const Game *game)
{
unsigned numbytes = strlen(GlobalState.line_number_marker) + 1 +
lineNumberChars(game->start_line) + 1 + lineNumberChars(game->end_line) + 1;
char *line_number_comment = (char *) malloc_or_die(numbytes);
sprintf(line_number_comment, "%s:%lu:%lu",
GlobalState.line_number_marker,
game->start_line,
game->end_line);
if(numbytes < strlen(line_number_comment) + 1) {
fprintf(GlobalState.logfile, "Internal error: insufficient space allocated in create_line_number_comment\n");
exit(1);
}
StringList *current_comment = save_string_list_item(NULL, line_number_comment);
CommentList *comment = (CommentList*) malloc_or_die(sizeof (*comment));
comment->comment = current_comment;
comment->next = NULL;
return comment;
}