shithub: moonfish

Download patch

ref: 8e9a8fe80cf9ab81aec8ff29dae9dc59ffbd5110
parent: 27b1ee1189feeecd2eefffe866426151cbe0d09f
author: zamfofex <zamfofex@twdb.moe>
date: Thu Mar 28 14:31:16 EDT 2024

add “battle” and “ribbon” CLI programs

--- a/.gitignore
+++ b/.gitignore
@@ -22,6 +22,8 @@
 !/tools/play.c
 !/tools/analyse.c
 !/tools/lichess.c
+!/tools/battle.c
+!/tools/ribbon.c
 !/tools/ugi.c
 !/tools/ugi-uci.c
 !/tools/uci-ugi.c
--- a/README.md
+++ b/README.md
@@ -52,9 +52,9 @@
 
 Conversely, you may also invoke your compiler by hand. (Feel free to replace `cc` with your compiler of choice.)
 
-Note: If your C implementation doesn’t support pthreads, but supports C11 threads, you can pass in `-Dmoonfish_c11_threads`.
+Note: If your C implementation doesn’t support pthreads, but supports C11 threads, you may pass in `-Dmoonfish_c11_threads`.
 
-Note: If your C implementation doesn’t support threads at all, you can pass in `-Dmoonfish_no_threads`.
+Note: If your C implementation doesn’t support threads at all, you may pass in `-Dmoonfish_no_threads`.
 
 ~~~
 cc -ansi -O3 -pthread -D_POSIX_C_SOURCE=199309L -o moonfish chess.c search.c main.c
@@ -72,13 +72,28 @@
 make ugi-uci uci-ugi
 ~~~
 
-usage
+The CLI tools, called “battle” and “ribbon” to (respectively) play a single game between two bots and to play a tournament between any given number of bots may also be compiled.
+
+~~~
+make battle ribbon
+~~~
+
+All such programs may also be compiled by using the default target, `all`. Note the the Lichess integration requires BearSSL and cJSON. (The other programs require no external libraries.)
+
+~~~
+make
+~~~
+
+using moonfish
 ---
 
-moonfish is a UCI bot, which means you can select it and use it with any UCI program (though see “limitations” above). You can invoke `./moonfish` to start its UCI interface.
+moonfish is a UCI bot, which means you may select it and use it with any UCI program (though see “limitations” above). You may invoke `./moonfish` to start its UCI interface.
 
-However, note that moonfish comes with its own UCI TUIs, called “play” and “analyse”. You can use them with any UCI engine you’d like!
+However, note that moonfish comes with its own UCI TUIs, called “play” and “analyse”. You may use them with any UCI bot you’d like!
 
+using “play” and “analyse”
+---
+
 To play against a UCI bot, use `./play` followed by the command of whichever bot you want to play against. The color of your pieces will be decided randomly by default.
 
 ~~~
@@ -108,7 +123,92 @@
 ./analyse lc0 --show-wdl
 ~~~
 
-`ugi-uci` can be used to let a UGI GUI communicate with a UCI bot, and conversely, `uci-ugi` can be used to let a UCI GUI communicate with a UGI bot.
+using “battle”
+---
+
+The “battle” CLI may be used to have two UCI or UGI bots play against each other. Each bot has to be specified between brackets (these ones: `[` and `]`).
+
+~~~
+./battle [ stockfish ] [ lc0 --preload ] > game.pgn
+~~~
+
+The bot specified first will play as white (or p1 for UGI) and the bot specified second will play as black (or p2 for UGI).
+
+The program will write a simplified PGN variant to stdout with the game as it happens. The moves will be in UCI/UGI format, rather than SAN.
+
+The program will also annouce to stderr the start and end of the game.
+
+If your bot’s command requires a `]` to be used as an argument, you may use double brackets instead, like `./battle [[ unusual ] --foo ]] [ stockfish ]`. You may use as many brackets as necessary, you just have to match them when closing.
+
+If your bot’s command starts with a dash, you may precede it by `--`, like `./battle [ -- -unusual ] [ stockfish ]`
+
+A FEN string may be passed in to the `battle` command, like `./battle --fen='...' [ stockfish ] [ lc0 ]`
+
+You may also pass in a time control, with time and increment both in milliseconds, like `./battle [ --time=6000+500 stockfish ] [ ./moonfish ]` Since each bot may have a different time control, this has to be specified within the backets for a specific bot. The default is 15 minutes with ten seconds of increment.
+
+You may also pass in `x` as the first character of the given time control to make it “fixed”, which means that it will be reset after every move. This can be used to set a fixed time per move, for example. For example, `./battle [ --time=1000+0 stockfish ] [ --time --time=x1000+0 ./moonfish ]` will set a countdown clock starting at one second for Stockfish, but allow one second for moonfish for every move.
+
+In order to use a UGI bot, you may also pass in `--protocol=ugi` to that bot, like `./battle [ --protocol=ugi some-bot ] [ stockfish ]`. If both bots are UGI, then the game is not assumed to be chess, and the bot playing as p1 will be queried for the status of the game.
+
+using “ribbon”
+---
+
+“ribbon” is a CLI for setting up a round‐robin tournament between multiple bots with the help of the “battle” program mentioned above and a makefile (currently requiring GNU Make).
+
+It will output a makefile to stdout, which may be used by GNU Make to start the tournament between the given bots.
+
+You may use the `-j` option of GNU Make to configure the number of concurrent games to play. Likewise, you may stop a given tournament with `ctrl-c` and continue it by simply running Make again.
+
+You may modify the makefile to prevent certain games from continuing to be played.
+
+Each bot’s command should be passed in as a single argument to `./ribbon`, including potential `--time=...` and `--protocol=...` options to be interpreted by “battle”.
+
+The file name of an opening book may be passed in, and the file should contain one FEN per line. Empty lines and comments starting with `#` are allowed.
+
+In addition, as a matter of convenience, you may specify `--time=...` to “ribbon” itself, which will be passed in verbatim to “battle”.
+
+~~~
+# prepare a tournament between Stockfish, Leela and moonfish:
+./ribbon stockfish lc0 ./moonfish > makefile
+
+# same as above, but with an argument passed in to Leela:
+./ribbon stockfish 'lc0 --preload' ./moonfish > makefile
+
+# same, but with the time control made explicit:
+./ribbon --time=18000+1000 stockfish 'lc0 --preload' ./moonfish > makefile
+
+# same, but with a different time control for Stockfish:
+# (mind the '--' separating "ribbon" options from the bot options for "battle")
+./ribbon --time=18000+1000 -- '--time=6000+0 stockfish' 'lc0 --preload' ./moonfish > makefile
+
+# start the tournament, with at most two games being played at any given time:
+make -j2
+
+# concatenate the resulting game PGNs into a single file.
+cat *.pgn > result.pgn
+~~~
+
+In addition, you may specify the “battle” command to “ribbon”, which must be done if “battle” is not installed on your system as `moonfish-battle`, like `./ribbon --battle=./battle stockfish lc0`
+
+~~~
+# prepare a tournament using the locally-built "battle":
+./ribbon --battle=./battle stockfish lc0 ./moonfish > makefile
+
+# set up a timeout of thirty seconds for each game:
+./ribbon --time=1000+0 --battle='timeout 30 moonfish-battle' stockfish lc0 ./moonfish > makefile
+
+# same as above, but with locally-built "battle":
+./ribbon --time=1000+0 --battle='timeout 30 ./battle' stockfish lc0 ./moonfish > makefile
+
+# pick up the tournament again on every failure (possibly due to timeout):
+while ! make
+do : ; done
+~~~
+
+using the UGI/UCI translators
+---
+
+`ugi-uci` may be used to let a UGI GUI communicate with a UCI bot, and conversely, `uci-ugi` may be used to let a UCI GUI communicate with a UGI bot.
 
 Simply pass the command of the bot as arguments to either of these tools, and it’ll translate it to be used by the respective GUI.
 
--- a/makefile
+++ b/makefile
@@ -15,7 +15,7 @@
 
 .PHONY: all clean install uninstall
 
-all: moonfish play lichess analyse uci-ugi ugi-uci
+all: moonfish play lichess analyse battle ribbon uci-ugi ugi-uci
 
 moonfish moonfish.exe moonfish.wasm: moonfish.h chess.c search.c main.c
 	$(moonfish_cc) -o $@ $(filter %.c,$^)
@@ -37,6 +37,8 @@
 	install -D -m 755 play $(BINDIR)/moonfish-play
 	install -D -m 755 lichess $(BINDIR)/moonfish-lichess
 	install -D -m 755 analyse $(BINDIR)/moonfish-analyse
+	install -D -m 755 battle $(BINDIR)/moonfish-battle
+	install -D -m 755 ribbon $(BINDIR)/moonfish-ribbon
 	install -D -m 755 ugi-uci $(BINDIR)/ugi-uci
 	install -D -m 755 uci-ugi $(BINDIR)/uci-ugi
 
--- /dev/null
+++ b/tools/battle.c
@@ -1,0 +1,480 @@
+/* moonfish is licensed under the AGPL (v3 or later) */
+/* copyright 2024 zamfofex */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+
+#include "../moonfish.h"
+#include "tools.h"
+
+struct moonfish_bot
+{
+	char *name;
+	FILE *in, *out;
+	long int time, increment;
+	int fixed_time;
+	int ugi;
+};
+
+struct moonfish_battle
+{
+	struct moonfish_chess chess;
+	struct moonfish_bot white, black;
+	char *fen;
+	int ugi;
+	char **moves;
+	int move_count;
+};
+
+static void moonfish_usage0(char *argv0)
+{
+	fprintf(stderr, "usage: %s <options>... '[' <options>... <cmd> <args>... ']' '[' <options>... <cmd> <args>... ']'\n", argv0);
+	fprintf(stderr, "options:\n");
+	fprintf(stderr, "   --fen=<FEN>   the starting position of the game\n");
+	fprintf(stderr, "bot options:\n");
+	fprintf(stderr, "   --protocol=<protocol>   either 'uci' or 'ugi' (default: 'uci')\n");
+	fprintf(stderr, "   --time=<time-control>   the time and increment for the bot in milliseconds\n");
+	fprintf(stderr, "   --name=<name>           name to be reported in the logs and in the output PGN\n");
+	exit(1);
+}
+
+static char *moonfish_option(char *name, char ***argv)
+{
+	size_t length;
+	
+	if (**argv == NULL) return NULL;
+	
+	if (!strcmp(**argv, name))
+	{
+		(*argv)++;
+		return *(*argv)++;
+	}
+	
+	length = strlen(name);
+	if (!strncmp(**argv, name, length) && (**argv)[length] == '=')
+		return *(*argv)++ + length + 1;
+	
+	return NULL;
+}
+
+static void moonfish_battle_options(struct moonfish_battle *battle, char ***argv)
+{
+	char *arg;
+	
+	for (;;)
+	{
+		arg = moonfish_option("--fen", argv);
+		if (arg != NULL)
+		{
+			battle->fen = arg;
+			continue;
+		}
+		
+		break;
+	}
+}
+
+static void moonfish_bot_options(char *argv0, struct moonfish_bot *bot, char ***argv)
+{
+	char *arg;
+	
+	for (;;)
+	{
+		arg = moonfish_option("--time", argv);
+		if (arg != NULL)
+		{
+			bot->fixed_time = 0;
+			if (arg[0] == 'x')
+			{
+				bot->fixed_time = 1;
+				arg++;
+			}
+			
+			if (sscanf(arg, "%ld%ld", &bot->time, &bot->increment) != 2)
+				moonfish_usage0(argv0);
+			
+			continue;
+		}
+		
+		arg = moonfish_option("--protocol", argv);
+		if (arg != NULL)
+		{
+			if (!strcmp(arg, "uci"))
+				bot->ugi = 0;
+			else if (!strcmp(arg, "ugi"))
+				bot->ugi = 1;
+			else
+				moonfish_usage0(argv0);
+			
+			continue;
+		}
+		
+		arg = moonfish_option("--name", argv);
+		if (arg != NULL)
+		{
+			bot->name = arg;
+			continue;
+		}
+		
+		break;
+	}
+}
+
+static void moonfish_bot_arguments(char *argv0, struct moonfish_bot *bot, char ***argv)
+{
+	size_t i, j;
+	char **options, **command, *value;
+	char *line, *arg;
+	char *buffer;
+	
+	if (**argv == NULL) moonfish_usage0(argv0);
+	
+	for (i = 0 ; (**argv)[i] != '\0' ; i++)
+		if ((**argv)[i] != '[') moonfish_usage0(argv0);
+	
+	if (i < 1) moonfish_usage0(argv0);
+	
+	bot->name = NULL;
+	
+	(*argv)++;
+	moonfish_bot_options(argv0, bot, argv);
+	
+	if (**argv != NULL)
+	{
+		if (!strcmp(**argv, "--"))
+			(*argv)++;
+		else if (***argv == '-')
+			moonfish_usage0(argv0);
+	}
+	
+	options = NULL;
+	
+	j = 0;
+	for (;;)
+	{
+		options = realloc(options, (j + 2) * sizeof *options);
+		if (options == NULL)
+		{
+			perror(argv0);
+			exit(1);
+		}
+		
+		if (**argv == NULL) moonfish_usage0(argv0);
+		options[j++] = *(*argv)++;
+		
+		if (strlen(**argv) != i) continue;
+		if (strspn(**argv, "]") != i) continue;
+		
+		if (**argv == NULL) moonfish_usage0(argv0);
+		(*argv)++;
+		break;
+	}
+	
+	if (j < 1) moonfish_usage0(argv0);
+	
+	options[j] = NULL;
+	
+	command = options;
+	
+	while (strchr(*command, '=') != NULL)
+		command++;
+	if (*command != NULL && !strcmp(*command, "--"))
+		command++;
+	
+	moonfish_spawn(argv0, command, &bot->in, &bot->out);
+	
+	if (bot->ugi) fprintf(bot->in, "ugi\n");
+	else fprintf(bot->in, "uci\n");
+	
+	for (;;)
+	{
+		line = moonfish_next(bot->out);
+		if (line == NULL) exit(1);
+		
+		arg = strtok_r(line, "\r\n\t ", &buffer);
+		if (arg == NULL) continue;
+		
+		if (!strcmp(arg, bot->ugi ? "ugiok" : "uciok")) break;
+		
+		if (bot->name != NULL) continue;
+		
+		if (strcmp(arg, "id")) continue;
+		
+		arg = strtok_r(NULL, "\r\n\t ", &buffer);
+		if (arg == NULL) continue;
+		if (strcmp(arg, "name")) continue;
+		
+		arg = strtok_r(NULL, "\r\n", &buffer);
+		if (arg == NULL) continue;
+		
+		bot->name = strdup(arg);
+		if (bot->name == NULL)
+		{
+			perror(argv0);
+			exit(1);
+		}
+	}
+	
+	if (bot->name == NULL) bot->name = command[0];
+	
+	j = 0;
+	for (;;)
+	{
+		value = strchr(options[j], '=');
+		if (value == NULL) break;
+		fprintf(bot->in, "setoption name %.*s value %s\n", (int) (value - options[j]), options[j], value + 1);
+		j++;
+	}
+	
+	free(options);
+	
+	fprintf(bot->in, "isready\n");
+	moonfish_wait(bot->out, "readyok");
+	
+	if (bot->ugi) fprintf(bot->in, "uginewgame\n");
+	else fprintf(bot->in, "ucinewgame\n");
+}
+
+static char *moonfish_bot_play(char *argv0, struct moonfish_battle *battle, struct moonfish_bot *bot)
+{
+	char *white, *black;
+	int i;
+	char *arg;
+	char *buffer;
+	struct moonfish_move move;
+	
+	struct timespec t0, t1;
+	
+	if (!bot->fixed_time && clock_gettime(CLOCK_MONOTONIC, &t0))
+	{
+		perror(argv0);
+		exit(1);
+	}
+	
+	fprintf(bot->in, "isready\n");
+	moonfish_wait(bot->out, "readyok");
+	
+	if (battle->fen)
+		fprintf(bot->in, "position fen %s", battle->fen);
+	else
+		fprintf(bot->in, "position startpos");
+	
+	if (battle->move_count > 0)
+	{
+		fprintf(bot->in, " moves");
+		for (i = 0 ; i < battle->move_count ; i++)
+			fprintf(bot->in, " %s", battle->moves[i]);
+	}
+	
+	fprintf(bot->in, "\n");
+	
+	if (bot->ugi)
+		white = "p1",
+		black = "p2";
+	else
+		white = "w",
+		black = "b";
+	
+	fprintf(bot->in, "go");
+	fprintf(bot->in, " %stime %ld", white, battle->white.time);
+	if (battle->white.increment > 0) fprintf(bot->in, " %sinc %ld", white, battle->white.increment);
+	fprintf(bot->in, " %stime %ld", black, battle->black.time);
+	if (battle->black.increment > 0) fprintf(bot->in, " %sinc %ld", black, battle->black.increment);
+	fprintf(bot->in, "\n");
+	
+	arg = moonfish_wait(bot->out, "bestmove");
+	arg = strtok_r(arg, "\r\n\t ", &buffer);
+	if (arg == NULL)
+	{
+		fprintf(stderr, "%s: invalid 'bestmove'\n", argv0);
+		exit(1);
+	}
+	arg = strdup(arg);
+	if (arg == NULL)
+	{
+		perror(argv0);
+		exit(1);
+	}
+	
+	battle->moves = realloc(battle->moves, (battle->move_count + 1) * sizeof *battle->moves);
+	if (battle->moves == NULL)
+	{
+		perror(argv0);
+		exit(1);
+	}
+	
+	battle->moves[battle->move_count++] = arg;
+	if (!battle->ugi)
+	{
+		moonfish_from_uci(&battle->chess, &move, arg);
+		moonfish_play(&battle->chess, &move);
+	}
+	
+	if (!bot->fixed_time)
+	{
+		if (clock_gettime(CLOCK_MONOTONIC, &t1))
+		{
+			perror(argv0);
+			exit(1);
+		}
+		
+		bot->time += bot->increment;
+		bot->time -= (t1.tv_sec - t0.tv_sec) * 1000;
+		bot->time -= (t1.tv_nsec - t0.tv_nsec) / 1000000;
+	}
+	
+	return arg;
+}
+
+static int moonfish_battle_play(char *argv0, struct moonfish_battle *battle)
+{
+	int white;
+	char *arg;
+	char *buffer;
+	
+	if (battle->ugi)
+	{
+		fprintf(battle->white.in, "isready\n");
+		moonfish_wait(battle->white.out, "readyok");
+		
+		fprintf(battle->white.in, "query result\n");
+		arg = moonfish_wait(battle->white.out, "response");
+		arg = strtok_r(arg, "\r\n\t ", &buffer);
+		if (arg == NULL)
+		{
+			fprintf(stderr, "%s: invalid 'response'\n", argv0);
+			exit(1);
+		}
+		
+		if (!strcmp(arg, "p1win"))
+		{
+			printf("1-0\n");
+			return 0;
+		}
+		if (!strcmp(arg, "p2win"))
+		{
+			printf("0-1\n");
+			return 0;
+		}
+		if (!strcmp(arg, "draw"))
+		{
+			printf("1/2-1/2\n");
+			return 0;
+		}
+		
+		fprintf(battle->white.in, "isready\n");
+		moonfish_wait(battle->white.out, "readyok");
+		
+		fprintf(battle->white.in, "query p1turn\n");
+		arg = moonfish_wait(battle->white.out, "response");
+		arg = strtok_r(arg, "\r\n\t ", &buffer);
+		if (arg == NULL)
+		{
+			fprintf(stderr, "%s: invalid 'response'\n", argv0);
+			exit(1);
+		}
+		
+		if (!strcmp(arg, "true"))
+			white = 1;
+		else
+			white = 0;
+	}
+	else
+	{
+		white = battle->chess.white;
+		if (moonfish_finished(&battle->chess))
+		{
+			if (moonfish_check(&battle->chess))
+			{
+				if (white) printf("0-1\n");
+				else printf("1-0\n");
+			}
+			else
+			{
+				printf("1/2-1/2\n");
+			}
+			
+			return 0;
+		}
+	}
+	
+	if (white)
+	{
+		arg = moonfish_bot_play(argv0, battle, &battle->white);
+		if (battle->white.time < -125)
+		{
+			printf("0-1 {timeout}\n");
+			return 0;
+		}
+		if (battle->white.time < 10) battle->white.time = 10;
+	}
+	else
+	{
+		arg = moonfish_bot_play(argv0, battle, &battle->black);
+		if (battle->black.time < -125)
+		{
+			printf("1-0 {timeout}\n");
+			return 0;
+		}
+		if (battle->black.time < 10) battle->black.time = 10;
+	}
+	
+	printf("%s ", arg);
+	
+	return 1;
+}
+
+int main(int argc, char **argv)
+{
+	static struct moonfish_battle battle;
+	
+	char *argv0;
+	
+	if (argc < 1) return 1;
+	
+	battle.fen = NULL;
+	
+	battle.white.time = 15 * 60000;
+	battle.white.increment = 10000;
+	battle.white.fixed_time = 0;
+	battle.white.ugi = 0;
+	
+	battle.black.time = 15 * 60000;
+	battle.black.increment = 10000;
+	battle.black.fixed_time = 0;
+	battle.black.ugi = 0;
+	
+	argv0 = *argv++;
+	moonfish_battle_options(&battle, &argv);
+	
+	moonfish_bot_arguments(argv0, &battle.white, &argv);
+	moonfish_bot_arguments(argv0, &battle.black, &argv);
+	
+	if (*argv != NULL) moonfish_usage0(argv0);
+	
+	battle.ugi = 0;
+	if (battle.white.ugi && battle.black.ugi)
+		battle.ugi = 1;
+	
+	if (!battle.ugi)
+	{
+		moonfish_chess(&battle.chess);
+		if (battle.fen && moonfish_fen(&battle.chess, battle.fen)) moonfish_usage0(argv0);
+	}
+	
+	battle.moves = NULL;
+	battle.move_count = 0;
+	
+	fprintf(stderr, "starting %s vs %s\n", battle.white.name, battle.black.name);
+	
+	printf("[White \"%s\"]\n", battle.white.name);
+	printf("[Black \"%s\"]\n", battle.black.name);
+	if (battle.fen != NULL) printf("[FEN \"%s\"]\n", battle.fen);
+	while (moonfish_battle_play(argv0, &battle)) { }
+	printf("\n");
+	
+	fprintf(stderr, "finished %s vs %s\n", battle.white.name, battle.black.name);
+	return 0;
+}
--- /dev/null
+++ b/tools/ribbon.c
@@ -1,0 +1,229 @@
+/* moonfish is licensed under the AGPL (v3 or later) */
+/* copyright 2024 zamfofex */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+
+static void moonfish_usage0(char *argv0)
+{
+	fprintf(stderr, "usage: %s <options>... <cmds>...\n", argv0);
+	fprintf(stderr, "options:\n");
+	fprintf(stderr, "   --openings=<file-name>   an opening book for the tournament (one FEN per line)\n");
+	fprintf(stderr, "   --battle=<command>       the command to make the two bots play each other\n");
+	fprintf(stderr, "   --time=<time-control>    default time control for each bot\n");
+	exit(1);
+}
+
+static char *moonfish_option(char *name, char ***argv)
+{
+	size_t length;
+	
+	if (**argv == NULL) return NULL;
+	
+	if (!strcmp(**argv, name))
+	{
+		(*argv)++;
+		return *(*argv)++;
+	}
+	
+	length = strlen(name);
+	if (!strncmp(**argv, name, length) && (**argv)[length] == '=')
+		return *(*argv)++ + length + 1;
+	
+	return NULL;
+}
+
+static void moonfish_options(char *argv0, FILE **openings, char **battle, char **time, char ***argv)
+{
+	char *arg;
+	
+	for (;;)
+	{
+		arg = moonfish_option("--openings", argv);
+		if (arg != NULL)
+		{
+			*openings = fopen(arg, "r");
+			if (*openings == NULL)
+			{
+				perror(argv0);
+				exit(1);
+			}
+			
+			continue;
+		}
+		
+		arg = moonfish_option("--battle", argv);
+		if (arg != NULL)
+		{
+			*battle = arg;
+			continue;
+		}
+		
+		arg = moonfish_option("--time", argv);
+		if (arg != NULL)
+		{
+			*time = arg;
+			continue;
+		}
+		
+		break;
+	}
+}
+
+static int moonfish_brackets(char *command)
+{
+	int count, max_count;
+	
+	max_count = 0;
+	count = 0;
+	
+	for (;;)
+	{
+		if (*command == ']')
+		{
+			count++;
+		}
+		else
+		{
+			if (count > max_count) max_count = count;
+			count = 0;
+		}
+		
+		if (*command++ == '\0') break;
+	}
+	
+	return max_count + 1;
+}
+
+int main(int argc, char **argv)
+{
+	static char line[2048];
+	
+	char *argv0;
+	FILE *openings;
+	int bot_count;
+	int brackets;
+	int i, j, k;
+	int opening_count;
+	char *battle, *time;
+	char *arg, *buffer;
+	
+	if (argc < 1) return 1;
+	
+	argv0 = *argv++;
+	
+	openings = NULL;
+	battle = "moonfish-battle";
+	time = NULL;
+	
+	moonfish_options(argv0, &openings, &battle, &time, &argv);
+	
+	if (argv[0] != NULL)
+	{
+		if (!strcmp(argv[0], "--"))
+			argv++;
+		else if (**argv == '-')
+			moonfish_usage0(argv0);
+	}
+	
+	printf("# makefile generated by '%s'\n\n", argv0);
+	printf(".PHONY: all\n\n");
+	printf("n = basename '$@' .pgn | cut -d- -f\n");
+	
+	/* todo: support BSD Make somehow... */
+	/* maybe even POSIX Make */
+	printf("n1 = $($(shell $n1))\n");
+	printf("n2 = $($(shell $n2))\n");
+	printf("n3 = $($(shell $n3))\n\n");
+	
+	bot_count = 0;
+	
+	for (;;)
+	{
+		if (argv[bot_count] == NULL) break;
+		
+		brackets = moonfish_brackets(argv[bot_count]);
+		
+		printf("bot%d = ", bot_count + 1);
+		for (i = 0 ; i  < brackets ; i++) printf("[");
+		printf(" ");
+		
+		if (time != NULL) printf("--time=%s ", time);
+		
+		printf("%s", argv[bot_count]);
+		
+		printf(" ");
+		for (i = 0 ; i  < brackets ; i++) printf("]");
+		printf("\n");
+		
+		bot_count++;
+	}
+	
+	if (bot_count == 0) fprintf(stderr, "warning: no bots established for tournament\n");
+	
+	printf("\n");
+	
+	if (openings == NULL)
+	{
+		printf("all:");
+		for (i = 0 ; i < bot_count ; i++)
+		for (j = 0 ; j < bot_count ; j++)
+		{
+			if (i == j) continue;
+			printf(" \\\n\tbot%d-bot%d.pgn", i + 1, j + 1);
+		}
+		printf("\n\n");
+		
+		printf(".DEFAULT:\n");
+		printf("\t@%s $(n1) $(n2) > $@.on\n", battle);
+		printf("\t@mv $@.on $@\n");
+		
+		return 0;
+	}
+	
+	opening_count = 0;
+	
+	for (;;)
+	{
+		errno = 0;
+		if (fgets(line, sizeof line, openings) == NULL)
+		{
+			if (errno == 0) break;
+			perror(argv0);
+			return 1;
+		}
+		
+		arg = line + strspn(line, "\r\n\t ");
+		if (arg[0] == '#') continue;
+		
+		arg = strtok_r(arg, "\r\n#", &buffer);
+		if (arg == NULL) continue;
+		
+		printf("opening%d = ", opening_count + 1);
+		printf("%s\n", arg);
+		
+		opening_count++;
+	}
+	
+	if (opening_count == 0) fprintf(stderr, "warning: empty opening book\n");
+	
+	printf("\n");
+	
+	printf("all:");
+	for (i = 0 ; i < bot_count ; i++)
+	for (j = 0 ; j < bot_count ; j++)
+	{
+		if (i == j) continue;
+		for (k = 0 ; k < opening_count ; k++)
+			printf(" \\\n\topening%d-bot%d-bot%d.pgn", k + 1, i + 1, j + 1);
+	}
+	printf("\n\n");
+	
+	printf(".DEFAULT:\n");
+	printf("\t@%s --fen='$(n1)' $(n2) $(n3) > $@.on\n", battle);
+	printf("\t@mv $@.on $@\n");
+	
+	return 0;
+}
--