Commit 4a3c0478 authored by Sam Moore's avatar Sam Moore

Revamped manager program and added manual page

Seperated controllers into AI and human controllers, which inherit from the same base class
Using "human" as an argument instead of an AI program will allow human player. However, the human player has to use the CLI.

I have tried playing a few games, and it is incredibly annoying using the CLI (especially when each turn was printed to stdout - now suppressed).
Usually I accidentally enter the wrong coordinates, or spend 30 seconds trying to work out the coordinates of a piece.
Then when I switch the focus, the SDL window goes blank, and I don't know what the hell is going on.
In conclusion: Should probably add GUI for human players!

Created Game class to manage playing the game, rather than hacking everything into main.cpp
Added argument switches for timeouts, graphics, output file, help, allowing illegal moves, revealing colours etc
Added result lines (output who actually wins... amazing!) The massive spamming output that used to be printed is suppressed (enable with -o stdout/file).

Created manual.txt which is the manual page for stratego (the manager program).
stratego --help will display the page (using "less" - should probably fix for systems without "less").

Changed tokens used for pieces from alphabet characters to digits for the ranked pieces, 's' for the Spy, 'B' for Bombs and 'F' for the Flag.
This makes things clearer. The Spy would be "10", except thats 2 characters, which is a bit awkward!
Didn't change the order of the enum, because thats just asking for trouble and besides, it works.
Changed stratego to output the characters for the piece, instead of an integer rank (except for Flag, Bomb and Spy, no difference).

Need to handle situations where a player has lost all their mobile pieces. Do they lose?
Currently they will be forced to make an illegal move, and the other player wins by "default".

Found mistake in forfax move score calculation that lead to moves having negative scores,
and hence occasionally an illegal move would be chosen as more valuable than legal moves.
Probably fixed. Illegal moves now score -1, so should NEVER be made! Ironically the change seemed to decrease forfax's performance against dummy.
Forfax still seems to make really stupid moves, and I can't see why. Occasionally it does something smart
(attacks Marshal with Spy just after the Marshal reveals itself), but I'm not sure how often these are coincidences.
Even with the devaluing of moves that don't end in combat, Forfax still gets into long cycles of repeated paths with no purpose.
And Forfax NEVER attacks Bombs or the Flag... even if thats all the enemy has, and even if the attacking piece would be a miner.

Updated web page. Considering replacing Protocol Description as is with that written for manual.txt, which I feel is clearer.

Need to make next git commit message shorter...
parent 53a66690
#Makefile for Stratego
CPP = g++ -Wall -pedantic -lSDL -lGL -g
OBJ = main.o controller.o program.o thread_util.o stratego.o graphics.o
OBJ = main.o controller.o ai_controller.o human_controller.o program.o thread_util.o stratego.o graphics.o game.o
BIN = stratego
......
#include <sstream>
#include "game.h"
#include "stratego.h"
#include "ai_controller.h"
using namespace std;
/**
* Queries the AI program to setup its pieces. Stores the setup in a st
* @implements Controller::QuerySetup
* @param
* @returns A MovementResult
*/
MovementResult AI_Controller::QuerySetup(const char * opponentName, std::string setup[])
{
switch (colour)
{
case Piece::RED:
if (!SendMessage("RED %s %d %d", opponentName, Game::theGame->theBoard.Width(), Game::theGame->theBoard.Height()))
return MovementResult::BAD_RESPONSE;
break;
case Piece::BLUE:
if (!SendMessage("BLUE %s %d %d", opponentName, Game::theGame->theBoard.Width(), Game::theGame->theBoard.Height()))
return MovementResult::BAD_RESPONSE;
break;
case Piece::NONE:
case Piece::BOTH:
return MovementResult::COLOUR_ERROR;
break;
}
for (int y = 0; y < 4; ++y)
{
if (!GetMessage(setup[y], timeout))
return MovementResult::BAD_RESPONSE;
}
return MovementResult::OK;
}
/**
* Queries the AI program to make a move
* @implements Controller::QueryMove
* @param buffer String which stores the AI program's response
* @returns A MovementResult which will be MovementResult::OK if a move was made, or MovementResult::NO_MOVE if the AI did not respond
*/
MovementResult AI_Controller::QueryMove(string & buffer)
{
if (!Running())
return MovementResult::NO_MOVE; //AI has quit
Game::theGame->theBoard.Print(output, colour);
if (!GetMessage(buffer,timeout))
{
return MovementResult::NO_MOVE; //AI did not respond (within the timeout). It will lose by default.
}
return MovementResult::OK; //Got the message
}
#ifndef AI_CONTROLLER_H
#define AI_CONTROLLER_H
#include "controller.h"
#include "program.h"
/**
* Class to control an AI program playing Stratego
* Inherits mostly from Program
*/
class AI_Controller : public Controller, private Program
{
public:
AI_Controller(const Piece::Colour & newColour, const char * executablePath, const double newTimeout = 2.0) : Controller(newColour), Program(executablePath), timeout(newTimeout) {}
virtual ~AI_Controller() {}
virtual MovementResult QuerySetup(const char * opponentName,std::string setup[]);
virtual MovementResult QueryMove(std::string & buffer);
virtual void Message(const char * message) {Program::SendMessage(message);}
private:
const double timeout; //Timeout in seconds for messages from the AI Program
};
#endif //AI_CONTROLLER_H
#ifndef COMMON_H
#define COMMON_H
#define GRAPHICS
#endif //COMMON_H
//EOF
#include <sstream>
#include "stratego.h"
#include "controller.h"
#include <sstream>
#include "game.h"
using namespace std;
/**
* Queries the AI program to setup its pieces
* @param opponentName - string containing the name/id of the opponent AI program
* @returns the result of the response
* Queries the player to setup their pieces
*
*/
MovementResult Controller::Setup(const char * opponentName)
{
int y;
string setup[4] = {"","","",""};
MovementResult query = this->QuerySetup(opponentName, setup);
if (query != MovementResult::OK)
return query;
int usedUnits[(int)(Piece::BOMB)];
for (int ii = 0; ii <= (int)(Piece::BOMB); ++ii)
usedUnits[ii] = 0;
int yStart = 0;
switch (colour)
{
case Piece::RED:
assert(SendMessage("RED %s %d %d", opponentName, Board::theBoard.Width(), Board::theBoard.Height()));
y = 0;
yStart = 0;
break;
case Piece::BLUE:
assert(SendMessage("BLUE %s %d %d", opponentName, Board::theBoard.Width(), Board::theBoard.Height()));
y = Board::theBoard.Height()-4;
yStart = Game::theGame->theBoard.Height()-4;
break;
case Piece::NONE:
case Piece::BOTH:
//Should never see this;
assert(false);
default:
return MovementResult::COLOUR_ERROR;
break;
}
int usedUnits[(int)(Piece::BOMB)];
for (int ii = 0; ii <= (int)(Piece::BOMB); ++ii)
usedUnits[ii] = 0;
//The setup is spread across 4 lines of the board - blue at the top, red at the bottom. AI has 2.5s for each line.
for (int ii=0; ii < 4; ++ii)
for (int y = 0; y < 4; ++y)
{
string line="";
if (!GetMessage(line, 2.5))
{
fprintf(stderr, "Timeout on setup\n");
return MovementResult::BAD_RESPONSE;
}
if ((int)(line.size()) != Board::theBoard.Width())
{
fprintf(stderr, "Bad length of \"%s\" on setup\n", line.c_str());
if ((int)setup[y].length() != Game::theGame->theBoard.Width())
return MovementResult::BAD_RESPONSE;
}
for (int x = 0; x < (int)(line.size()); ++x)
for (int x = 0; x < Game::theGame->theBoard.Width(); ++x)
{
Piece::Type type = Piece::GetType(line[x]);
Piece::Type type = Piece::GetType(setup[y][x]);
if (type != Piece::NOTHING)
{
//fprintf(stderr, "x y %d %d\n", x, y+ii);
// fprintf(stderr, "Found unit of type '%c' (%d '%c') %d vs %d\n", line[x], (int)(type), Piece::tokens[(int)(type)], usedUnits[(int)(type)], Piece::maxUnits[(int)type]);
/// fprintf(stderr, "Marshal is %d '%c', flag is %d '%c'\n", (int)Piece::MARSHAL, Piece::tokens[(int)(Piece::MARSHAL)], (int)Piece::FLAG, Piece::tokens[(int)(Piece::FLAG)]);
usedUnits[(int)(type)] += 1;
usedUnits[(int)(type)]++;
if (usedUnits[type] > Piece::maxUnits[(int)type])
{
fprintf(stderr, "Too many units of type %c\n", Piece::tokens[(int)(type)]);
return MovementResult::BAD_RESPONSE;
}
Board::theBoard.AddPiece(x, y+ii, type, colour);
Game::theGame->theBoard.AddPiece(x, yStart+y, type, colour);
}
}
}
}
if (usedUnits[(int)Piece::FLAG] <= 0)
{
return MovementResult::BAD_RESPONSE; //You need to include a flag!
}
return MovementResult::OK;
}
/**
* Queries the AI program to respond to a state of Board::theBoard
* Queries the player to respond to a state of Game::theGame->theBoard
* @param buffer String which is used to store the player's responses
* @returns The result of the response and/or move if made
*/
MovementResult Controller::MakeMove(string & buffer)
{
if (!Running())
return MovementResult::NO_MOVE; //AI has quit
Board::theBoard.Print(output, colour);
buffer.clear();
if (!GetMessage(buffer,2))
{
return MovementResult::NO_MOVE; //AI did not respond. It will lose by default.
}
MovementResult query = this->QueryMove(buffer);
if (query != MovementResult::OK)
return query;
int x; int y; string direction="";
stringstream s(buffer);
s >> x;
......@@ -134,19 +108,19 @@ MovementResult Controller::MakeMove(string & buffer)
else
{
fprintf(stderr, "BAD_RESPONSE \"%s\"\n", buffer.c_str());
return MovementResult::BAD_RESPONSE; //AI gave bogus direction - it will lose by default.
return MovementResult::BAD_RESPONSE; //Player gave bogus direction - it will lose by default.
}
int multiplier = 1;
if (s.peek() != EOF)
s >> multiplier;
MovementResult moveResult = Board::theBoard.MovePiece(x, y, dir, multiplier, colour);
MovementResult moveResult = Game::theGame->theBoard.MovePiece(x, y, dir, multiplier, colour);
s.clear(); s.str("");
//I stored the ranks in the wrong order; rank 1 is the marshal, 2 is the general etc...
//So I am reversing them in the output... great work
s << (Piece::BOMB - moveResult.attackerRank) << " " << (Piece::BOMB - moveResult.defenderRank);
s << Piece::tokens[(int)(moveResult.attackerRank)] << " " << Piece::tokens[(int)(moveResult.defenderRank)];
switch (moveResult.type)
{
case MovementResult::OK:
......@@ -174,10 +148,9 @@ MovementResult Controller::MakeMove(string & buffer)
}
if (!Board::LegalResult(moveResult))
if (Game::theGame->allowIllegalMoves && !Board::LegalResult(moveResult))
return MovementResult::OK; //HACK - Legal results returned!
else
return moveResult;
}
......@@ -2,28 +2,40 @@
#define CONTROLLER_H
#include "stratego.h"
#include "program.h"
#include <string>
/**
* Class to control an AI program for a game of Stratego
* Inherits most features from the Program class
* Class to control a player for Stratego
* Abstract base class
*/
class Controller : public Program
class Controller
{
public:
Controller(const Piece::Colour & newColour, const char * executablePath) : Program(executablePath), colour(newColour) {}
Controller(const Piece::Colour & newColour) : colour(newColour) {}
virtual ~Controller() {}
MovementResult Setup(const char * opponentName); //Requests the AI program for the initial positioning of its pieces.
MovementResult Setup(const char * opponentName);
MovementResult MakeMove(std::string & buffer); //Queries the AI program for a response to the state of Board::theBoard
MovementResult MakeMove(std::string & buffer);
const Piece::Colour colour; //Colour identifying the side of the AI program.
void Message(std::string & buffer) {Message(buffer.c_str());}
virtual void Message(const char * string) = 0;
virtual MovementResult QuerySetup(const char * opponentName, std::string setup[]) = 0;
virtual MovementResult QueryMove(std::string & buffer) = 0;
const Piece::Colour colour;
};
#endif //CONTROLLER_H
#include "game.h"
using namespace std;
Game* Game::theGame = NULL;
Game::Game(const char * redPath, const char * bluePath, const bool enableGraphics, double newStallTime, const bool allowIllegal, FILE * newLog, const Piece::Colour & newReveal) : red(NULL), blue(NULL), turn(Piece::RED), theBoard(10,10), graphicsEnabled(enableGraphics), stallTime(newStallTime), allowIllegalMoves(allowIllegal), log(newLog), reveal(newReveal), turnCount(0)
{
static bool gameCreated = false;
if (gameCreated)
{
if (log != NULL)
fprintf(log, "ERROR - Game has already been created!\n");
exit(EXIT_FAILURE);
}
gameCreated = true;
Game::theGame = this;
signal(SIGPIPE, Game::HandleBrokenPipe);
if (graphicsEnabled && (!Graphics::Initialised()))
Graphics::Initialise("Stratego", theBoard.Width()*32, theBoard.Height()*32);
if (strcmp(redPath, "human") == 0)
red = new Human_Controller(Piece::RED, graphicsEnabled);
else
red = new AI_Controller(Piece::RED, redPath);
if (strcmp(bluePath, "human") == 0)
blue = new Human_Controller(Piece::BLUE, graphicsEnabled);
else
blue = new AI_Controller(Piece::BLUE, redPath);
}
Game::~Game()
{
fprintf(stderr, "Killing AI\n");
delete red;
delete blue;
if (log != NULL && log != stdout && log != stderr)
fclose(log);
}
bool Game::Setup(const char * redName, const char * blueName)
{
for (int y = 4; y < 6; ++y)
{
for (int x = 2; x < 4; ++x)
{
theBoard.AddPiece(x,y,Piece::BOULDER, Piece::NONE);
}
for (int x = 6; x < 8; ++x)
{
theBoard.AddPiece(x,y,Piece::BOULDER, Piece::NONE);
}
}
MovementResult redSetup = red->Setup(blueName);
MovementResult blueSetup = blue->Setup(redName);
if (redSetup != MovementResult::OK)
{
if (blueSetup != MovementResult::OK)
{
if (log != NULL)
fprintf(log, "BOTH players give invalid setup!\n");
red->Message("ILLEGAL");
blue->Message("ILLEGAL");
}
else
{
if (log != NULL)
fprintf(log, "Player RED gave an invalid setup!\n");
red->Message("ILLEGAL");
blue->Message("DEFAULT");
}
return false;
}
else if (blueSetup != MovementResult::OK)
{
if (log != NULL)
fprintf(log, "Player BLUE gave an invalid setup!\n");
red->Message("DEFAULT");
blue->Message("ILLEGAL");
return false;
}
return true;
}
void Game::Wait(double wait)
{
if (wait <= 0)
return;
TimerThread timer(wait*1000000); //Wait in seconds
timer.Start();
if (!graphicsEnabled)
{
while (!timer.Finished());
timer.Stop();
return;
}
while (!timer.Finished())
{
SDL_Event event;
while (SDL_PollEvent(&event))
{
switch (event.type)
{
case SDL_QUIT:
timer.Stop();
exit(EXIT_SUCCESS);
break;
}
}
}
timer.Stop();
}
void Game::HandleBrokenPipe(int sig)
{
if (theGame->turn == Piece::RED)
{
theGame->logMessage("Game ends on RED's turn - REASON: ");
theGame->blue->Message("DEFAULT");
}
else if (theGame->turn == Piece::BLUE)
{
theGame->logMessage("Game ends on BLUE's turn - REASON: ");
theGame->red->Message("DEFAULT");
}
else
{
theGame->logMessage("Game ends on ERROR's turn - REASON: ");
}
theGame->logMessage("SIGPIPE - Broken pipe (AI program may have segfaulted)\n");
if (Game::theGame->graphicsEnabled && theGame->log == stdout)
{
theGame->logMessage("CLOSE WINDOW TO EXIT\n");
Game::theGame->theBoard.Draw(Piece::BOTH);
while (true)
{
SDL_Event event;
while (SDL_PollEvent(&event))
{
switch (event.type)
{
case SDL_QUIT:
exit(EXIT_SUCCESS);
break;
}
}
}
}
else
{
if (theGame->log == stdout)
{
theGame->logMessage( "PRESS ENTER TO EXIT\n");
theGame->theBoard.Print(theGame->log);
while (fgetc(stdin) != '\n');
}
}
exit(EXIT_SUCCESS);
}
void Game::PrintEndMessage(const MovementResult & result)
{
if (turn == Piece::RED)
{
logMessage("Game ends on RED's turn - REASON: ");
}
else if (turn == Piece::BLUE)
{
logMessage("Game ends on BLUE's turn - REASON: ");
}
else
{
logMessage("Game ends on ERROR's turn - REASON: ");
}
switch (result.type)
{
case MovementResult::OK:
logMessage("Status returned OK, unsure why game halted...\n");
break;
case MovementResult::DIES:
logMessage("Status returned DIES, unsure why game halted...\n");
break;
case MovementResult::KILLS:
logMessage("Status returned KILLS, unsure why game halted...\n");
break;
case MovementResult::BOTH_DIE:
logMessage("Status returned BOTH_DIE, unsure why game halted...\n");
break;
case MovementResult::NO_BOARD:
logMessage("Board does not exit?!\n");
break;
case MovementResult::INVALID_POSITION:
logMessage("Coords outside board\n");
break;
case MovementResult::NO_SELECTION:
logMessage("Move does not select a piece\n");
break;
case MovementResult::NOT_YOUR_UNIT:
logMessage("Selected piece belongs to other player\n");
break;
case MovementResult::IMMOBILE_UNIT:
logMessage("Selected piece is not mobile (FLAG or BOMB)\n");
break;
case MovementResult::INVALID_DIRECTION:
logMessage("Selected unit cannot move that way\n");
break;
case MovementResult::POSITION_FULL:
logMessage("Attempted move into square occupied by allied piece\n");
break;
case MovementResult::VICTORY:
logMessage("Captured the flag\n");
break;
case MovementResult::BAD_RESPONSE:
logMessage("Unintelligable response\n");
break;
case MovementResult::NO_MOVE:
logMessage("Did not make a move (may have exited)\n");
break;
case MovementResult::COLOUR_ERROR:
logMessage("Internal controller error - COLOUR_ERROR\n");
break;
case MovementResult::ERROR:
logMessage("Internal controller error - Unspecified ERROR\n");
break;
}
if (graphicsEnabled && log == stdout)
{
logMessage("CLOSE WINDOW TO EXIT\n");
theBoard.Draw(Piece::BOTH);
while (true)
{
SDL_Event event;
while (SDL_PollEvent(&event))
{
switch (event.type)
{
case SDL_QUIT:
exit(EXIT_SUCCESS);