/*
This file is part of Fantom.
Fantom 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.
Fantom 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 Fantom. If not, see <http://www.gnu.org/licenses/>.
*/
package cz.matfyz.aai.fantom.server;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Properties;
import java.util.Random;
import org.apache.log4j.Logger;
import cz.matfyz.aai.fantom.game.Actor;
import cz.matfyz.aai.fantom.game.Graph;
import cz.matfyz.aai.fantom.game.TransportType;
import cz.matfyz.aai.fantom.message.ClientType;
import cz.matfyz.aai.fantom.message.Message;
import cz.matfyz.aai.fantom.message.MessageMove;
import cz.matfyz.aai.fantom.message.MessageMoveBase;
import cz.matfyz.aai.fantom.message.MessageQuit;
import cz.matfyz.aai.fantom.message.MessageStart;
import cz.matfyz.aai.fantom.message.MessageUpdate;
import cz.matfyz.aai.fantom.message.MessageMoveBase.ActorMove;
import cz.matfyz.aai.fantom.utils.ParserException;
/**
* The game server. Handles communication between the clients, maintains
* its current state and produces full logs of the game.
*/
public class Server {
/**
* The name of the logger used by the server.
*/
protected static final String LOGGER_NAME = "cz.matfyz.aai.fantom.server";
/**
* The name of the file that contains configuration of the server.
*/
protected static final String CONFIGURATION_NAME = "fantom.properties";
/**
* The name of the property in the file {@link #CONFIGURATION_NAME} that
* contains the initial value of the seed of the random number generator.
*/
protected static final String CONFIGURATION_PROPERTY_SEED = "fantom.server.seed";
/**
* The name of the property in the file {@link #CONFIGURATION_NAME} that
* specifies if the messages between the server and the clients should be
* logged to a file.
*/
protected static final String CONFIGURATION_PROPERTY_LOG_MESSAGES = "fantom.server.log.messages";
/**
* The logger used by the server.
*/
protected Logger logger = Logger.getLogger(LOGGER_NAME);
/**
* The client that moves the detectives.
*/
protected Client detectiveClient;
/**
* The command-line to start the detective client.
*/
protected String[] detectiveCommandLine;
/**
* The number of games won by the detectives.
*/
protected int detectiveWins;
/**
* The client that moves the phantom.
*/
protected Client phantomClient;
/**
* The command-line to start the phantom client.
*/
protected String[] phantomCommandLine;
/**
* The number of games won by the phantom.
*/
protected int phantomWins;
/**
* The game graph used in the game. May be set to <code>null</code>,
* when no game is active.
*/
protected Graph graph;
/**
* Set to <code>true</code> if the client should log messages going
* between the client and the server.
*/
protected boolean logMessages;
/**
* The game graph as it was loaded from the source file. This field
* only contains the original graph, the working copy for the game
* is stored in {@link #graph}.
*/
protected Graph sourceGraph;
/**
* The number of games the clients should play on the graph.
*/
protected int gameCount;
/**
* The name of the run. This name is used to prefix all log files
* created by the server.
*/
protected String runName;
/**
* Source of non-determinism. The generator is replaced by a new
* instance, if a seed is provided in the configuration.
*/
protected Random rnd = new Random();
/**
* Returns the name of this run.
* @return the name of this run.
*/
public String getRunName() {
return this.runName;
}
/**
* Initializes the clients. Starts the client processes and reads the
* message with their names.
*
* @throws ServerException if there is a problem with starting the clients.
*/
protected void initClients() throws ServerException {
logger.info("Creating the detective client");
detectiveClient = new Client(detectiveCommandLine, ClientType.DETECTIVE, getRunName(), logMessages);
logger.info("Creating the phantom client");
phantomClient = new Client(phantomCommandLine, ClientType.PHANTOM, getRunName(), logMessages);
// Start the detective client, wait for the initial message
logger.info("Starting the detective client process");
detectiveClient.startProcess();
logger.info("Detective client process started");
// Start the phantom client, wait for the initial message
logger.info("Starting the phantom client process");
phantomClient.startProcess();
logger.info("Phantom process started");
}
/**
* Kills the client processes. This method should only be used in case
* that the server is terminated unexpectedly.
*/
public void killClientProcesses() {
if(detectiveClient != null)
detectiveClient.killProcess();
if(phantomClient != null)
phantomClient.killProcess();
}
/** Starts a game.
* @param phantoms List of phantoms in the game.
* @param detectives List of detectives still in the game.
* @return A winner if game is over, or <code>null</code> if nobody won during the initial phase.
*/
protected MessageUpdate startGame(List<Actor> phantoms, List<Actor> detective) throws ServerException {
logger.info("Initializing a new game");
MessageStart msgStart = new MessageStart(graph);
logger.debug("Sending message 'start' to the detectives");
detectiveClient.sendMessage(msgStart);
logger.trace("Message sent");
logger.debug("Sending message 'start' to the phantom");
phantomClient.sendMessage(msgStart);
logger.trace("Message sent");
// Place the detectives.
logger.trace("Receiving actor placement from the detectives");
Message msg = detectiveClient.receiveMessage(graph);
if(!(msg instanceof MessageMove))
throw new ProtocolException("'move' message was expected from the detective client", detectiveClient);
logger.debug("Message received");
MessageMove detectivePlacements = (MessageMove) msg;
try {
logger.trace("Placing detectives on the graph");
graph.verifyActorPlacement(detectivePlacements, detectiveClient);
graph.placeActors(detectivePlacements);
logActorPlacement(detectiveClient, detectivePlacements);
logger.trace("Detectives were placed");
}
catch(ClientException e) {
e.setClient(detectiveClient);
throw e;
}
// Inform the phantom about the initial placement of the detectives.
logger.debug("Sending information about detective placement to the phantom");
msg = new MessageUpdate(graph, detectivePlacements.getMoves(), null);
phantomClient.sendMessage(msg);
logger.trace("Message was sent");
// Place the phantom.
logger.debug("Receiving actor placement from the phantom");
msg = phantomClient.receiveMessage(graph);
if(!(msg instanceof MessageMove))
throw new ProtocolException("'move' message was expected from the detective client", phantomClient);
logger.trace("Message received");
MessageMove phantomPlacement = (MessageMove) msg;
try {
logger.debug("Placing phantoms on the graph");
graph.verifyActorPlacement(phantomPlacement, phantomClient);
graph.placeActors(phantomPlacement);
logActorPlacement(phantomClient, phantomPlacement);
logger.trace("Phantoms were placed");
}
catch(ClientException e) {
e.setClient(phantomClient);
throw e;
}
processCapturedPhantoms(phantoms);
if(phantoms.isEmpty()) {
logger.info("All phantoms were captured, detectives won the game");
return new MessageUpdate(graph, phantomPlacement.getMoves(), ClientType.DETECTIVE);
}
logger.debug("Letting the phantom start.");
msg = new MessageUpdate(graph, new ActorMove[0], null);
phantomClient.sendMessage(msg);
logger.trace("Phantom starts playing.");
return null;
}
/**
* Updates the list of phantoms according to the phantoms captured in the
* current round.
* @param phantoms the list of phantoms to be updated.
*/
protected void processCapturedPhantoms(List<Actor> phantoms) {
for(Actor phantom : graph.capturedPhantoms()) {
logger.info(String.format("Phantom %s was captured at node %s", phantom.getId(), phantom.getCurrentPosition().getId()));
phantom.capture();
phantoms.remove(phantom);
}
}
/** Runs a single round of a game.
* @param phantoms List of phantoms in the game.
* @param detectives List of detectives still in the game.
* @param round Current round.
* @return A winner if game is over, or <code>null</code> if nobody won in this round.
*/
protected MessageUpdate runRound(List<Actor> phantoms, List<Actor> detectives, int round) throws ServerException {
logger.info(String.format("Playing round %d.", round));
// Get the phantom moves.
logger.debug("Reading movement information from the phantom");
Message msg = phantomClient.receiveMessage(graph);
if(!(msg instanceof MessageMove))
throw new ProtocolException("'move' message was expected from the detective client", phantomClient);
logger.trace("Movement information from the phantom was received");
MessageMove phantomMove = (MessageMove) msg;
Collection<Actor> immobilePhantoms = null;
try {
logActorMovement(phantomClient, phantomMove);
logger.debug("Updating actor positions in the graph");
immobilePhantoms = graph.moveActors(phantoms, phantomMove);
logger.trace("Actor positions were updated");
}
catch(ProtocolException e) {
e.setClient(phantomClient);
throw e;
}
// Mark all phantoms captured in this turn as captured, and remove them from the
// list of active phantoms.
logger.debug("Processing captured phantoms");
processCapturedPhantoms(phantoms);
// Inform the detectives.
logger.debug("Sending detectives information about the moves of the phantom");
MessageMoveBase.ActorMove[] pams = phantomMove.getMoves();
if(!graph.getPhantomReveals().contains(round)) {
// Hide the target.
MessageMoveBase.ActorMove[] hpams = new MessageMoveBase.ActorMove[pams.length];
for(int i = 0; i < pams.length; i++) {
hpams[i] = new MessageMoveBase.ActorMove(pams[i].getActor(), null, pams[i].getTransportType());
}
pams = hpams;
} else {
if(immobilePhantoms.size() > 0) {
// There are immobile phantoms. We need to add them to the message to reveal
// their position.
MessageMoveBase.ActorMove[] moves = new MessageMoveBase.ActorMove[pams.length + immobilePhantoms.size()];
int pos = 0;
for(int i = 0; i < pams.length; i++) {
moves[pos++] = pams[i];
}
for(Actor phantom : immobilePhantoms) {
moves[pos++] = new ActorMove(phantom, phantom.getCurrentPosition(), null);
}
pams = moves;
}
}
if(phantoms.isEmpty()) {
return new MessageUpdate(graph, pams, ClientType.DETECTIVE);
}
msg = new MessageUpdate(graph, pams, null);
detectiveClient.sendMessage(msg);
logger.trace("Message was sent");
// Get the moves of detectives.
logger.debug("Reading movement information from detectives");
msg = detectiveClient.receiveMessage(graph);
if(!(msg instanceof MessageMove))
throw new ProtocolException("'move' message was expected from the detective client", detectiveClient);
logger.trace("Movement information from the phantom was received");
MessageMove detectiveMoves = (MessageMove) msg;
try {
logActorMovement(detectiveClient, detectiveMoves);
logger.debug("Updating actor positions in the graph");
graph.moveActors(detectives, detectiveMoves);
logger.trace("Actor positions were updated");
}
catch(ProtocolException e) {
e.setClient(detectiveClient);
throw e;
}
// First process phantoms captured in the last move...
logger.debug("Processing captured phantoms");
processCapturedPhantoms(phantoms);
if(phantoms.isEmpty()) {
//return ClientType.DETECTIVE;
return new MessageUpdate(graph, detectiveMoves.getMoves(), ClientType.DETECTIVE);
}
logger.debug("Distributing tickets used by detectives to phantoms");
for(MessageMove.ActorMove m : detectiveMoves.getMoves()) {
TransportType transport = m.getTransportType();
if(transport.isUsedByPhantom()) {
int lucky = rnd.nextInt(phantoms.size()); //TODO: With multiple phantoms, how should the tickets be distributed?
Actor luckyActor = phantoms.get(lucky);
logger.info(String.format("Phantom %s gets one ticket for transport %s", luckyActor.getId(), transport.getName()));
phantoms.get(lucky).addTicket(transport);
}
}
// Inform the phantom.
logger.debug("Sending phantom information about movement of the detectives");
ClientType winner = null;
// If this is the last round, the phantom escaped and won the game
if(round == graph.getGameLength()) {
logger.info("Detectives did not catch the phantoms, phantoms won the game");
winner = ClientType.PHANTOM;
return new MessageUpdate(graph, detectiveMoves.getMoves(), winner);
}
MessageUpdate detectiveUpdate = new MessageUpdate(graph, detectiveMoves.getMoves(), winner);
phantomClient.sendMessage(detectiveUpdate);
logger.trace("Message was sent");
logger.debug("Nobody won the current round");
return null;
}
/**
* Runs a single game.
* @return Returns a winner of the game.
* @throws ServerException In case of server errors.
* @throws ProtocolException In case some violation of rules is detected.
*/
public MessageUpdate runGame() throws ServerException {
this.graph = (Graph)sourceGraph.clone();
List<Actor> phantoms = new ArrayList<Actor>(graph.getActors().size());
List<Actor> detectives = new ArrayList<Actor>(graph.getActors().size());
for(Actor a : graph.getActors()) {
if(a.isDetective()) {
detectives.add(a);
} else {
phantoms.add(a);
}
}
MessageUpdate result = startGame(phantoms, detectives);
if(result != null) {
return result;
}
for(int round = 1; round <= graph.getGameLength(); round++) {
result = runRound(phantoms, detectives, round);
logger.info(String.format("Round %d ended", round));
if(result != null) {
return result;
}
}
throw new IllegalStateException("runRound in the last round did not return the winner");
}
/**
* Sends the clients the 'end' message.
*
* @param winner the type of the client that won the game.
* @throws ServerException if there is a problem with sending the message.
*/
protected void gameOver(MessageUpdate winner) throws ServerException {
if(winner == null)
throw new IllegalArgumentException("winner must not be null");
if(winner.getWinner() == null)
throw new IllegalStateException("No winner was specified for the last message");
switch(winner.getWinner()) {
case DETECTIVE:
detectiveWins++;
break;
case PHANTOM:
phantomWins++;
}
Message detectiveMsg = null;
Message phantomMsg = null;
if(winner.getMoves().length == 0) {
detectiveMsg = winner;
phantomMsg = winner;
}
else {
ClientType recipient = winner.getMoves()[0].getActor().getClientType();
if(recipient != ClientType.DETECTIVE) {
detectiveMsg = winner;
phantomMsg = new MessageUpdate(graph, new ActorMove[0], winner.getWinner());
} else {
detectiveMsg = new MessageUpdate(graph, new ActorMove[0], winner.getWinner());
phantomMsg = winner;
}
}
detectiveClient.sendMessage(detectiveMsg);
phantomClient.sendMessage(phantomMsg);
}
/**
* Sends the clients the 'quit' message.
*
* @throws ServerException if there is a problem with sending the message.
*/
protected void quit() throws ServerException {
logger.info("Sending the 'quit' message to clients");
Message msg = new MessageQuit();
logger.debug("Sending the 'quit' message to detectives");
detectiveClient.sendMessage(msg);
logger.debug("Sending the 'quit' message to phantoms");
phantomClient.sendMessage(msg);
logger.debug("The message was sent");
}
/**
* Runs the server.
*/
public void run() throws ServerException {
initClients();
for(int game = 0; game < gameCount; game++) {
logger.info(String.format("Starting game %d", game));
MessageUpdate winner = runGame();
gameOver(winner);
logger.info(String.format("Game %d ended", game));
}
logger.info(String.format("Games won by the detectives: %d", detectiveWins));
logger.info(String.format("Games won by the phantom: %d", phantomWins));
quit();
}
/**
* Writers information about placement of agents to the log.
*
* @param client the client that did the placement.
* @param placement the message that contains the placement.
*/
protected void logActorPlacement(Client client, MessageMove placement) {
if(client == null)
throw new IllegalArgumentException("client must not be null");
if(placement == null)
throw new IllegalArgumentException("placment must not be null");
logger.debug(String.format("Placing actors (%s)", client.getClientName()));
for(ActorMove move : placement.getMoves()) {
logger.info(String.format("Actor: %s, node: %s", move.getActor().getId(), move.getTargetNode().getId()));
}
}
/**
* Writes information about movement of agents to the log.
*
* @param client the client that did the movement.
* @param movement the message that contains the movement.
*/
protected void logActorMovement(Client client, MessageMove movement) {
if(client == null)
throw new IllegalArgumentException("client must not be null");
if(movement == null)
throw new IllegalArgumentException("movement must not be null");
logger.debug(String.format("Moving actors (%s)", client.getClientName()));
for(ActorMove move : movement.getMoves()) {
Actor actor = move.getActor();
logger.info(String.format("Actor %s moved from node %s to node %s",
actor.getId(), actor.getCurrentPosition().getId(),
move.getTargetNode().getId()));
}
}
/**
* Initializes a new server.
*
* @param commandLineDetective command line for starting the detective client.
* @param commandLinePhantom command line for starting the phantom client.
*/
public Server(Graph sourceGraph, int gameCount, String[] commandLineDetective, String[] commandLinePhantom, String runName) {
if(commandLineDetective == null || commandLineDetective.length == 0)
throw new IllegalArgumentException("The command-line for the detectives was not supplied");
if(commandLinePhantom == null || commandLinePhantom.length == 0)
throw new IllegalArgumentException("The command-line for the phantom was not supplied");
if(runName == null || runName.isEmpty())
throw new IllegalArgumentException("runName must not be null");
if(sourceGraph == null)
throw new IllegalArgumentException("sourceGraph must not be null");
if(gameCount <= 0)
throw new IllegalArgumentException("gameCount must be a positive number");
this.detectiveCommandLine = commandLineDetective;
this.detectiveWins = 0;
this.phantomCommandLine = commandLinePhantom;
this.phantomWins = 0;
this.runName = runName;
this.sourceGraph = sourceGraph;
this.gameCount = gameCount;
loadConfiguration();
}
/**
* Loads the configuration of the server.
*/
protected void loadConfiguration() {
Properties config = new Properties();
InputStream configStream = null;
try {
configStream = ClassLoader.getSystemResourceAsStream(CONFIGURATION_NAME);
config.load(configStream);
String logMessagesStr = config.getProperty(CONFIGURATION_PROPERTY_LOG_MESSAGES);
logMessages = Boolean.parseBoolean(logMessagesStr);
String seedStr = config.getProperty(CONFIGURATION_PROPERTY_SEED);
int seed = Integer.parseInt(seedStr);
if(seed != 0)
this.rnd = new Random(seed);
}
catch(Exception err) {
logger.error("Could not read the configuration", err);
}
finally {
try {
if(configStream != null)
configStream.close();
}
catch(IOException e) {
logger.error("Could not close input stream", e);
}
}
}
/**
* Prints usage information for the server to stderr.
*/
public static void printUsage() {
System.err.println("Usage: Server graph games name {phantom command line} \";\" {detective command line}");
System.err.println(" graph is the name of the file with the graph");
System.err.println(" games is the number of games");
System.err.println(" what follows is the phantom and detective command lines separated");
System.err.println(" by a single semicolon");
}
/**
* Parses the command-line arguments and starts the server.
*
* @param args the command-line arguments.
*/
public static void main(String[] args) {
if(args.length < 5) {
printUsage();
return;
}
// Read the number of games played in the tournament
int gameCount = 0;
try {
gameCount = Integer.parseInt(args[1]);
}
catch(NumberFormatException e) {
System.err.println("The number of games is not a valid number");
printUsage();
return;
}
// Reads the name of the tournament (used in names of log files)
String runName = args[2];
if(runName.isEmpty()) {
System.err.println("Run name must not be empty");
printUsage();
return;
}
// Extract command-lines for both clients
int semicolonIndex = Arrays.asList(args).indexOf(";");
if(semicolonIndex < 0) {
System.err.println("The separator between the detective and phantom command lines was not found");
printUsage();
return;
}
String[] commandLinePhantom = Arrays.copyOfRange(args, 3, semicolonIndex);
if(commandLinePhantom.length == 0) {
System.err.println("The phantom command line was not specified");
printUsage();
return;
}
String[] commandLineDetective = Arrays.copyOfRange(args, semicolonIndex + 1, args.length);
if(commandLineDetective.length == 0) {
System.err.println("The detective command line was not specified");
printUsage();
return;
}
// Load the graph file
File graphFile = null;
try {
graphFile = new File(args[0]);
}
catch(Exception err) {
System.err.println("Invalid graph file name");
printUsage();
return;
}
Graph graph = null;
try {
graph = new Graph(graphFile);
}
catch(IOException e) {
System.err.println("Could not read the graph file");
e.printStackTrace();
} catch (ParserException e) {
System.err.println("The graph file has invalid format at line " + e.getLineNumber());
e.printStackTrace();
}
Server server = new Server(graph, gameCount, commandLineDetective, commandLinePhantom, runName);
try {
server.run();
}
catch(ServerException e) {
System.err.println("The server failed: " + e.getMessage());
e.printStackTrace();
server.killClientProcesses();
}
}
}