package net.sf.colossus.client;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.sf.colossus.ai.AI;
import net.sf.colossus.ai.SimpleAI;
import net.sf.colossus.common.Constants;
import net.sf.colossus.common.OptionObjectProvider;
import net.sf.colossus.common.Options;
import net.sf.colossus.common.WhatNextManager;
import net.sf.colossus.game.BattleCritter;
import net.sf.colossus.game.BattlePhase;
import net.sf.colossus.game.BattleUnit;
import net.sf.colossus.game.Creature;
import net.sf.colossus.game.Engagement;
import net.sf.colossus.game.EntrySide;
import net.sf.colossus.game.Game;
import net.sf.colossus.game.Legion;
import net.sf.colossus.game.MovementClientSide;
import net.sf.colossus.game.Phase;
import net.sf.colossus.game.Player;
import net.sf.colossus.game.PlayerColor;
import net.sf.colossus.game.Proposal;
import net.sf.colossus.game.SummonInfo;
import net.sf.colossus.game.actions.Recruitment;
import net.sf.colossus.game.actions.Summoning;
import net.sf.colossus.gui.ClientGUI;
import net.sf.colossus.server.CustomRecruitBase;
import net.sf.colossus.server.GameServerSide;
import net.sf.colossus.server.IServer;
import net.sf.colossus.server.Server;
import net.sf.colossus.server.VariantSupport;
import net.sf.colossus.util.Glob;
import net.sf.colossus.util.InstanceTracker;
import net.sf.colossus.util.ResourceLoader;
import net.sf.colossus.util.Split;
import net.sf.colossus.util.ViableEntityManager;
import net.sf.colossus.variant.BattleHex;
import net.sf.colossus.variant.CreatureType;
import net.sf.colossus.variant.IVariant;
import net.sf.colossus.variant.MasterBoardTerrain;
import net.sf.colossus.variant.MasterHex;
import net.sf.colossus.variant.Variant;
import net.sf.colossus.xmlparser.TerrainRecruitLoader;
/**
* Lives on the client side and handles all communication
* with the server. It talks to the Server via the network protocol
* and to Client side classes locally, but to all GUI related classes
* it should only communicate via ClientGUI class.
* There is one client per player.
*
* TODO Handle GUI related issues purely via ClientGUI
*
* TODO All GUI classes should talk to the server purely through
* ClientGUI which handles it via the Client.
*
* TODO the logic for the battles could probably be separated from the
* rest of this code. At the moment the battle logic seems to bounce
* back and forth between BattleBoard (which is really a GUI class) and
* this class.
*
* TODO this class also has the functionality of a GameClientSide class,
* which should be separated and ideally moved up into the {@link Game}
* class. The whole {@link IOracle} interface is part of that.
* One approach would be moving code from {@link GameServerSide}
* up into {@link Game} and then reuse it here in the matching methods,
* then inlining it into the calling code. Another one would be creating
* the GameClientSide for now and relocating code there.
* ==> Clemens march 2009: I started the GameClientSide approach :)
*
* @author David Ripton
* @author Romain Dolbeau
*/
@SuppressWarnings("serial")
public final class Client implements IClient, IOracle, IVariant,
OptionObjectProvider
{
private static final Logger LOGGER = Logger.getLogger(Client.class
.getName());
/**
* This "server" is the access to the connector object which actually
* acts for us as server.
*
* Right now this is always a SocketClientThread as deputy (relay)
* which forwards everything that we do/tell, to the Server.
* Perhaps one day this could either be a SocketConnection or e.g.
* a Queue type of connection for local Clients...
*/
private IServer server;
/** The object that actually handles the physical server communication for
* this client. Issues related to set up and tear down of the connection
* are handled via this access to the (right now) SocketClientThread.
*/
private IServerConnection connection;
/**
* InactivityWatchdog needs this, to retrigger an event to make
* the AI kick in
*/
private EventExecutor eventExecutor;
/**
* A first start to get rid of the static-access-everywhere to
* ResourceLoader.
* ResourceLoader is used to "load" images, variant files, readme files
* physically (from disk, or from remote file server thread).
*/
private final ResourceLoader resourceLoader;
/** Client constructor sets this to true if something goes wrong with the
* SocketClientThread initialization. I wanted to avoid have the Client
* constructor throw an exception, because that caused problems in
* Java 1.4 with a "created but not run thread" which was then never
* cleaned up and thus JVM did not exit by itself.
* TODO perhaps that is now fixed in Java 1.5 ?
* I plan to change the whole "when/how SCT is created" soon anyway...
*/
private final boolean failed = false;
/** Replay during load of a saved game is ongoing. Client must NOT react
* (not even redraw) on any of those messages, they are mostly sent to
* rebuild the predict split data.
*/
private boolean replayOngoing = false;
/**
* Redo of the events since last commit phase is ongoing.
* Needed right now only for "if redo ends, set flag to prevent the
* setupXxxxxPhase methods to clear the undo stack.
*/
private boolean redoOngoing = false;
/** This can be an actual ClientGUI, or a NullClientGUI (which does simply
* nothing, so that we don't need to check for null everywhere).
*/
private final IClientGUI gui;
// Per-client and per-player options.
private final Options options;
/** At first time we get "all player info", they are created; at all
* later calls just update them. So this flag here tells us whether
* it's the first time (=true) or not any more (=false).
*/
private boolean playersNotInitialized = true;
/**
* Player who owns this client.
*
* TODO should be final but can't be until the constructor gets all the data
* needed
*/
private PlayerClientSide owningPlayer;
private boolean playerAlive = true;
private boolean clockIsTicking = false;
private final Autoplay autoplay;
/**
* The game in progress.
*/
private final GameClientSide game;
/**
* Starting marker color of player who owns this client.
*
* TODO most likely redundant with owningPlayer.getColor()
*/
// private PlayerColor color;
// This ai is either the actual ai player for an AI player, but is also
// used by human clients for the autoXXX actions.
private final AI ai;
// TODO: could go into owningPlayer, BUT tricky right now as long as
// owningPlayer is created twice (once fake and later for real)...
private MovementClientSide movement;
private BattleMovement battleMovement;
private final Server localServer;
// This client is a spectator
private final boolean spectator;
/**
* This client is the very special internal spectator with the name
* as defined in Constants.
* The idea of this internal spectator is: it can run in standby in
* any game, e.g. also on the public server, and detect discrepancies
* between local state and updateCreatureCount or playerInfo from
* Server. This is part of the work, to replace "all details need to
* be broadcasted all the time" with "Client side does the bookkeeping
* autonomously; so for quite a while it would do bookkeeping and the
* updates are still sent but just for checking, and any discrepancy
* detected can/should be fixed.
*/
private final boolean internalSpectator;
/**
* Constants modelling the party who closed this client.
*/
private enum ClosedByConstant
{
NOT_CLOSED, CLOSED_BY_SERVER, CLOSED_BY_CLIENT
}
private ClosedByConstant closedBy = ClosedByConstant.NOT_CLOSED;
private final static int MAX_RECONNECT_ATTEMPTS = 6;
private final static int RECONNECT_RETRY_INTERVAL = 10;
// XXX temporary until things are synchronized
private boolean tookMulligan;
private int numSplitsThisTurn;
private int delay = -1;
/** For battle AI. */
private List<CritterMove> bestMoveOrder = null;
private List<CritterMove> failedBattleMoves = null;
private final Hashtable<CreatureType, Integer> recruitReservations = new Hashtable<CreatureType, Integer>();
/**
* Once we got dispose from server (or user initiated it himself),
* we'll ignore it if we we get it from server again
* - it's then up to the user to do some "disposing" action.
*/
private boolean gotDisposeAlready = false;
private boolean disposeInProgress = false;
/**
* Everytime we request server to sync data (typically after reconnect),
* we pass with a request counter, so that we can distinct the
* syncCompleted responses.
*/
private int syncRequestCounter = 0;
/**
* Create a Client object and other related objects
*
* @param host The host to which SocketClientThread shall connect
* @param port The port to which SocketClientThread shall connect
* @param playerName Name of the player (might still be one of the
* <byXXX> templates
* @param playerType Type of player, e.g. Human, Network, or some
* concrete AI type (but not "AnyAI").
* Given type must include the package name.
* @param whatNextMgr The main controller over which to handle what to do
* next when this game is over and exiting
* @param theServer The Server object, if this is a local client
* @param byWebClient If true, this was instantiated by a WebClient
* @param noOptionsFile E.g. AIs should not read/save any options file
* @param createGUI Whether to create a GUI (AI's usually not, but server
* might override that e.g. in stresstest)
* @param spectator true to join as spectator, false as real player
*
*/
public static synchronized Client createClient(String host, int port,
String playerName, String playerType, WhatNextManager whatNextMgr,
Server theServer, boolean byWebClient, boolean noOptionsFile,
boolean createGUI, boolean spectator) throws ConnectionInitException
{
/* TODO Clients on same machine could share the instance
* (proper synchronization needed, of course).
*/
ResourceLoader loader;
boolean remote;
Variant variant;
if (theServer == null)
{
loader = new ResourceLoader(host, port + 1);
remote = true;
}
else
{
loader = new ResourceLoader(null, 0);
remote = false;
}
IServerConnection conn = SocketClientThread.createConnection(host,
port, playerName, remote, spectator);
// TODO For now, loading the variant is needed only if remote client.
// ( => theServer is null; theServer != null is the server object in
// same JVM).
// One day in future, every client should perhaps do this to get his
// own instance of the variant (instead of static access).
// Or local clients get the Variant object passed in to here...
if (theServer == null)
{
String variantName = conn.getVariantNameForInit();
// enforce loading of variant in any case, so that we get XML
// files and README from remote server - this way at least if
// changes were only to xml files, server and client see
// the same data.
// Pictures are currently loaded from own jar files in any
// case; but if a pic is missing, it's merely an empty chit,
// not data inconsistency which might cause mysterious errors.
LOGGER.info("Oh, we are a remote client"
+ " - let's make sure we really get text files from server.");
VariantSupport.unloadVariant();
variant = VariantSupport.loadVariantByName(variantName, true);
}
else
{
variant = theServer.getGame().getVariant();
}
return new Client(playerName, playerType, whatNextMgr, theServer,
byWebClient, noOptionsFile, createGUI, loader, conn, variant,
spectator);
}
/**
* Client is the main hub for info exchange on client side.
* @param playerName Name of the player (might still be one of the
* <byXXX> templates
* @param playerType Type of player, e.g. Human, Network, or some
* concrete AI type (but not "AnyAI").
* Given type must include the package name.
* @param whatNextMgr The main controller over which to handle what to do
* next when this game is over and exiting
* @param theServer The Server object, if this is a local client
* @param byWebClient If true, this was instantiated by a WebClient
* @param noOptionsFile E.g. AIs should not read/save any options file
* @param createGUI Whether to create a GUI (AI's usually not, but server
* might override that e.g. in stresstest)
* @param resLoader The ResourceLoader object that gives us access to
* load images, files etc (from disk or from server)
* @param conn The connection to server (so far, SocketClientThread)
* @param variant The variant instance
* @param spectator true to join as spectator, false as real player
*/
/* TODO Now Client creates the Game (GameClientSide) instance.
* So far it creates it mostly with dummy info; should do better.
* - for example, create first SocketClientThread, and as first
* answer to connect gets the Variant name, and use that
* for game creation. So when Client constructor is completed
* also Game and Variant are proper.
* (problem would still be ... player count and names...)
*
* TODO try to make the Client class agnostic of the network or not question by
* having the SCT outside and behaving like a normal server -- that way it
* would be easier to run the local clients without the detour across the
* network and the serialization/deserialization of all objects
*
* TODO Make player type typesafe
*/
public Client(String playerName, String playerType,
WhatNextManager whatNextMgr, Server theServer, boolean byWebClient,
boolean noOptionsFile, boolean createGUI, ResourceLoader resLoader,
IServerConnection conn, Variant variant, boolean spectator)
{
assert playerName != null;
this.spectator = spectator;
this.internalSpectator = (playerName
.equals(Constants.INTERNAL_DUMMY_CLIENT_NAME));
Collection<String> playerNamesList = conn.getPreliminaryPlayerNames();
String[] playerNames = new String[playerNamesList.size()];
int i = 0;
for (String name : playerNamesList)
{
playerNames[i++] = name;
}
// TODO can we change all Game constructors to List or Collection?
// Those Array[]s are annoying...
// TODO playerNames is still a dummy argument
this.game = new GameClientSide(variant, playerNames);
// TODO give it to constructor right away? Not changing it right now,
// first do the "create SCT and Variant (and Game??) outside Client
// and pass them in" and see then whether it's better to create the
// Game outside ( = then we can't give Client to Game constructor)
// or create Game inside Client (then we can pass in the Client).
game.setClient(this);
conn.startThread();
this.connection = conn;
connection.setClient(this);
this.resourceLoader = resLoader;
LOGGER.finest("Got ResourceLoader: " + resourceLoader.toString());
// TODO this is currently not set properly straight away, it is fixed
// in updatePlayerInfo(..) when the PlayerInfos are initialized.
// Should really happen here, but doesn't yet since we don't have
// all players (not even as names) yet
this.owningPlayer = new PlayerClientSide(getGame(), playerName, 0);
// type setting is needed because we ask owningPlayer.isAI() below
// TODO set type in constructor
// (the whole player info setup needs fixing...)
this.owningPlayer.setType(playerType);
this.ai = createAI(playerType);
ViableEntityManager.register(this, "Client " + playerName);
InstanceTracker.register(this, "Client " + playerName);
options = new Options(playerName, noOptionsFile);
if (createGUI)
{
this.gui = new ClientGUI(this, options, whatNextMgr);
}
else
{
this.gui = new NullClientGUI(this, options, whatNextMgr);
}
// Need to load options early so they don't overwrite server options.
options.loadOptions();
autoplay = new Autoplay(options);
// This is needed because we do not do syncAutoPlay any more,
// and many places rely on "if (Options.getOption(... .autoXXXX)
// returning true because autoPlay was set for AI type players.
// And it needs to be in place already when the pickColor and
// pickMarker requests come from server:
options.setOption(Options.autoPlay, this.owningPlayer.isAI());
this.server = connection.getIServer();
if (!spectator)
{
server.joinGame(playerName);
}
else
{
server.watchGame();
}
TerrainRecruitLoader.setCaretaker(getGame().getCaretaker());
CustomRecruitBase.addCaretakerClientSide(getGame().getCaretaker());
this.localServer = theServer;
gui.setStartedByWebClient(byWebClient);
}
/**
* Create the AI for this Client. If type is some (concrete) AI type,
* create that type of AI (then this is an AI player).
* Otherwise, create a SimpleAI as default (used by Human or Remote
* clients for the autoplay functionality).
*
* @param playerType Type of player for which to create an AI
* @return Some AI object, according to the situation
*/
// TODO move (partly) to AI package, e.g. as static method
private AI createAI(String playerType)
{
LOGGER.log(Level.FINEST, "Creating the AI player type " + playerType);
AI createdAI = null;
String createType = playerType;
if (createType.endsWith(Constants.anyAI))
{
LOGGER.severe("Invalid player type " + createType
+ " on client side - server is supposed to select the "
+ "actual AI type for random choosing.");
}
// Non-AI (= human and remote) players use some AI for autoplay:
if (!createType.endsWith("AI"))
{
createType = Constants.aiPackage + Constants.autoplayAI;
}
// TODO Can it still happen that we get a unqualified AI class name?
// Or does server nowadays always send proper fully qualified name?
if (!createType.startsWith(Constants.aiPackage))
{
String name = getOwningPlayer().getName();
LOGGER.warning("Needed to add package for AI type? Type="
+ createType + ", Player name=" + name);
createType = Constants.aiPackage + createType;
}
try
{
// TODO these seem to be classes of either AI or Client, there
// should be a common ancestor
// NOPE . The one is the "first argument of the constructor
// is of type Client. The other is the Client object passed
// to that constructor.
// TODO Anyway, this could be done better, perhaps moved to
// AI package, using typesafe enums that do the mapping from name
// to class explicitly?
Class<?>[] classArray = new Class<?>[1];
classArray[0] = Class.forName("net.sf.colossus.client.Client");
Object[] objArray = new Object[1];
objArray[0] = this;
createdAI = (AI)Class.forName(createType)
.getDeclaredConstructor(classArray).newInstance(objArray);
}
catch (Exception ex)
{
LOGGER.log(Level.SEVERE, "Failed to create for client "
+ owningPlayer.getName() + " the to " + createType, ex);
}
if (createdAI == null)
{
createdAI = new SimpleAI(this);
}
return createdAI;
}
public void appendToConnectionLog(String s)
{
// in rare cases with scratch-reconnects we might not have a gui or
// connection log window yet.
if (isReplayOngoing() || gui == null)
{
return;
}
gui.appendToConnectionLog(s);
}
public boolean isSctAlreadyDown()
{
return connection.isAlreadyDown();
}
public boolean isRemote()
{
return (localServer == null);
}
public boolean isSpectator()
{
return spectator;
}
/*
* Following ones are used for the client side timeout (if player
* has not done anything for certain time ((left keyboard??)), then
* something is done in his behalf so that game is not stuck/lost
* for other players.
*/
public boolean needsWatchdog()
{
// For Debugging/Development only this particular one
// has watchdog, other's not, so that "some other" player
// can be active (and thus this one here does not need to be)
if (getOwningPlayer().getName().equals("watchdogtest")
|| getOwningPlayer().getName().equals("localwatchdogtest"))
{
return true;
}
if (getOwningPlayer().isAI())
{
return false;
}
return options.getOption(Options.inactivityTimeout);
}
public boolean hasWatchdog()
{
return gui.hasWatchdog();
}
public void setClockTicking(boolean val)
{
clockIsTicking = val;
}
public boolean isClockTicking()
{
return clockIsTicking;
}
public boolean isTheInternalSpectator()
{
return internalSpectator;
}
public boolean isAutoplayActive()
{
return autoplay.isAutoplayActive();
}
public Autoplay getAutoplay()
{
return autoplay;
}
public boolean getAutoSplit()
{
return autoplay.autoSplit();
}
// TODO can this be replaced with "!owningPlayer.isDead()" ?
// Only critical issue AFAIS is that owningPlayer is not properly
// initialized right away from the start (re-assigned later).
public boolean isAlive()
{
assert owningPlayer != null : "owningPlayer is null, "
+ "can't ask whether alive or not!";
assert owningPlayer.isDead() != playerAlive : "playerAlive gives "
+ "different result than owningPlayer.isDead()!";
return playerAlive;
}
private boolean paused = false;
public boolean isPaused()
{
return paused;
}
private String currentLegionMarkerId = null;
public void setCurrentLegionMarkerId(String MarkerId)
{
currentLegionMarkerId = MarkerId;
}
public String getCurrentLegionMarkerId()
{
return currentLegionMarkerId;
}
public void setPauseState(boolean newState)
{
if (isRemote())
{
LOGGER.warning("setPauseState should not be possible "
+ "in remote client!");
}
else
{
paused = newState;
localServer.setPauseState(paused);
}
}
public void enforcedDisconnect()
{
connection.enforcedConnectionException();
}
public boolean ensureThatConnected()
{
if (isConnected())
{
return true;
}
else
{
notifyThatNotConnected();
return false;
}
}
public void notifyThatNotConnected()
{
String waitText = "";
if (isConnectRoundOngoing())
{
waitText = "\n\nPlease wait until Reconnect is completed.";
}
showMessageDialog("NOTE: You are currently not connected "
+ " to the server!" + waitText);
}
// for debugging/development purposes
public void enforcedDisconnectByServer()
{
localServer.enforcedDisconnectClient(owningPlayer.getName());
// localServer.enforcedDisconnectClient("remote");
}
/**
* is != -1 only from the point on when client abandons the connection,
* until sync is completed. When sync is completed, it's re-set back
* to -1.
*/
private int lastMsgNr = -1;
private IServerConnection previousConn;
public boolean isConnected()
{
return previousConn == null;
}
public void abandonCurrentConnection()
{
if (lastMsgNr == -1)
{
previousConn = connection;
int got = previousConn.abandonAndGetMessageCounter();
if (got == -2)
{
LOGGER.warning("Connection was abandoned already "
+ "before, got msgCounter -2 ???");
}
else
{
lastMsgNr = got;
}
}
}
/**
*
* @param automatic true if was triggered automatically e.g. by a Socket Exception,
* false if triggered manually (e.g. MasterBoard File menu).
*/
public void tryReconnect(boolean automatic)
{
String cause = automatic ? "automatically" : "manually";
LOGGER.info("Trying reconnect (" + cause + " triggered)");
appendToConnectionLog("Trying reconnect (" + cause + " triggered)");
int attempt = 1;
IServerConnection conn = null;
do
{
try
{
conn = SocketClientThread.recreateConnection(previousConn);
}
catch (ConnectionInitException e)
{
LOGGER.warning("Reconnect #" + attempt + " attempted ("
+ cause + ") but got ConnectionInitException " + e);
if (attempt < MAX_RECONNECT_ATTEMPTS)
{
appendToConnectionLog("PROBLEM: Reconnect attempt #"
+ attempt + " failed; will retry after "
+ RECONNECT_RETRY_INTERVAL + " seconds ("
+ (MAX_RECONNECT_ATTEMPTS - attempt)
+ " attempts left)");
WhatNextManager.sleepFor(RECONNECT_RETRY_INTERVAL * 1000);
attempt++;
}
else
{
break;
}
}
}
while (conn == null);
// Okay: Reconnect succeeded!
if (conn != null)
{
appendToConnectionLog("Connection succeeded, "
+ "initiating synchronization...");
gotDisposeAlready = false;
this.connection = conn;
this.server = connection.getIServer();
connection.setClient(this);
connection.startThread();
int oldMsgsLeft;
int i = 0;
while ((oldMsgsLeft = previousConn.getDisposedQueueLen()) > 0
&& i < 1)
{
i++;
appendToConnectionLog("WARNING: Previous connection has still "
+ oldMsgsLeft
+ " items to process (handling that is not implemented yet...)!");
// WhatNextManager.sleepFor(1000);
}
connection.requestSyncDelta(lastMsgNr, ++syncRequestCounter);
LOGGER.info("Connection re-established. "
+ "Waiting for synchronization to complete.");
}
else
{
String message = "Trying to reconnect ("
+ cause
+ " triggered) failed "
+ attempt
+ " times - giving up."
// Add the rest only for automatically triggered reconnects.
+ (automatic ? "\n\n"
+ "You may try it again (from File Menu) after a few seconds."
+ "\nBut if that does not succeed, well, bad luck:-("
: "");
gui.showMessageDialogAndWait(message);
}
}
public void guiTriggeredTryReconnect()
{
LOGGER.info("Menu-triggered (= manual) reconnect!");
fireOneReconnectRunnable(false);
}
private final Object oneConnectAttemptsRoundMutex = new Object();
private Runnable oneConnectAttemptsRound = null;
private void setConnectAttemptsRoundCompleted()
{
synchronized (oneConnectAttemptsRoundMutex)
{
oneConnectAttemptsRound = null;
}
}
public boolean isConnectRoundOngoing()
{
synchronized (oneConnectAttemptsRoundMutex)
{
return (oneConnectAttemptsRound != null);
}
}
/**
* Creates a runnable that executes one reconnect round (several attempts)
*/
private void fireOneReconnectRunnable(final boolean automatic)
{
synchronized (oneConnectAttemptsRoundMutex)
{
if (oneConnectAttemptsRound != null)
{
appendToConnectionLog("One reconnect-attempt round already "
+ "ongoing - starting a new one omitted.");
return;
}
oneConnectAttemptsRound = new Runnable()
{
public void run()
{
abandonCurrentConnection();
tryReconnect(automatic);
setConnectAttemptsRoundCompleted();
}
};
}
new Thread(oneConnectAttemptsRound).start();
}
public void tellSyncCompleted(int syncRequestNumber)
{
LOGGER.info("Synchronization #" + syncRequestNumber + " completed!");
lastMsgNr = -1;
previousConn = null;
if (!isSpectator())
{
gui.actOnReconnectCompleted();
}
if (isSpectator() && syncRequestNumber == 0)
{
// this is the initial sync during connect for watching,
// skip displaying anything
}
else
{
gui.appendToConnectionLog("Synchronization #" + syncRequestNumber
+ " completed!");
}
}
/** Take a mulligan. */
public void mulligan()
{
gui.undoAllMoves(); // XXX Maybe move entirely to server
tookMulligan = true;
server.mulligan();
}
public void requestExtraRoll()
{
server.requestExtraRoll();
}
// XXX temp
public boolean tookMulligan()
{
return tookMulligan;
}
public void requestExtraRollApproval(String requestorName, int requestId)
{
LOGGER.finest("Client " + getOwningPlayer().getName()
+ " is asked to approve extra roll request... - answering true");
if (isAutoplayActive())
{
sendExtraRollRequestResponse(true, requestId);
}
else if (requestorName.equals(getOwningPlayer().getName()))
{
/*
* If server asks from the requestor itself, it means that no
* other client is able to can approve.
* User should now negotiate in chat and answer respond to
* server accordingly.
*/
LOGGER.finest("Server asks me, the requestor, for approval "
+ " - that means no other client can approve.");
gui.askExtraRollApproval(requestorName, true, requestId);
}
else
{
/* Ask the player/user for approval; it will send the answer back
* asynchronously
*/
gui.askExtraRollApproval(requestorName, false, requestId);
}
}
/**
* Player or AI has answered, send the response to server
* @param approved
* @param requestId
*/
public void sendExtraRollRequestResponse(boolean approved, int requestId)
{
server.extraRollResponse(approved, requestId);
}
public void askSuspendConfirmation(String requestorName, int timeout)
{
LOGGER.fine("User " + requestorName
+ " requests to suspend the game, timeout=" + timeout);
if (getOwningPlayer().isAI() || autoplay.isAutoplayActive())
{
LOGGER.finest("Server asks us for suspend approval. Auto-yes.");
suspendResponse(true);
}
else if (requestorName.equals(getOwningPlayer().getName()))
{
// If server asks from the requestor itself, it means that no
// other client is able to can approve.
LOGGER.finest("Server asks me, the requestor, for approval "
+ " - that means no other client can approve.");
suspendResponse(true);
}
else
{
gui.askSuspendConfirmation(requestorName, timeout);
}
}
public void suspendResponse(boolean approved)
{
if (game.isSuspended())
{
LOGGER.finest("Game already suspended, probably called during "
+ "dispose()? Not sending response.");
}
else if (server == null)
{
LOGGER.warning("In client " + getOwningPlayer().getName()
+ ": in suspendResponse(), server NULL ?");
}
else
{
LOGGER.finest("Client " + getOwningPlayer().getName()
+ ": in suspendResponse: sending response " + approved);
server.suspendResponse(approved);
}
}
public void doCheckServerConnection()
{
server.checkServerConnection();
}
public void doCheckAllConnections(String requestingClientName)
{
server.checkAllConnections(requestingClientName);
}
public void relayedPeerRequest(String requestingClientName)
{
LOGGER.finest("In client " + this.owningPlayer.getName()
+ " we received, that peer " + requestingClientName
+ " requests a reply.");
server.peerRequestProcessed(requestingClientName);
}
/** Upon request with checkServerConnection, server sends a confirmation.
* This method here processes the confirmation.
*/
public synchronized void serverConfirmsConnection()
{
gui.serverConfirmsConnection();
}
public void peerRequestReceivedBy(String respondingPlayerName, int queueLen)
{
LOGGER.info("Got RECEIVED confirmation from " + respondingPlayerName
+ ", queueLen=" + queueLen);
}
public void peerRequestProcessedBy(String respondingPlayerName)
{
LOGGER.info("Got PROCESSED confirmation from " + respondingPlayerName);
}
public void locallyInitiateSaveGame(String filename)
{
localServer.initiateSaveGame(filename);
}
public void initiateSuspend(boolean save)
{
server.requestToSuspendGame(save);
}
public boolean getFailed()
{
return failed;
}
/** Resolve engagement in land. */
public void engage(MasterHex hex)
{
server.engage(hex);
}
public Legion getMyEngagedLegion()
{
if (isMyLegion(getAttacker()))
{
return getAttacker();
}
else if (isMyLegion(getDefender()))
{
return getDefender();
}
return null;
}
public void concede()
{
concede(getMyEngagedLegion());
}
private void concede(Legion legion)
{
if (legion != null)
{
server.concede(legion);
}
}
private void doNotConcede(Legion legion)
{
server.doNotConcede(legion);
}
/** Cease negotiations and fight a battle in land. */
private void fight(MasterHex hex)
{
server.fight(hex);
}
boolean engagementStartupOngoing = false;
public void setEngagementStartupOngoing(boolean val)
{
engagementStartupOngoing = val;
}
public void tellEngagement(MasterHex hex, Legion attacker, Legion defender)
{
if (isMyLegion(attacker) || isMyLegion(defender))
{
setEngagementStartupOngoing(true);
}
game.createEngagement(hex, attacker, defender);
gui.tellEngagement(attacker, defender, getTurnNumber());
}
public void tellEngagementResults(Legion winner, String method,
int points, int turns)
{
setEngagementStartupOngoing(false);
gui.actOnTellEngagementResults(winner, method, points, turns);
game.clearEngagementData();
gui.actOnEngagementCompleted();
}
/** This player quits the whole game. The server needs to always honor
* this request, because if it doesn't players will just drop
* connections when they want to quit in a hurry. */
public void withdrawFromGame()
{
if (!game.isGameOver() && !owningPlayer.isDead())
{
server.withdrawFromGame();
}
}
public void tellMovementRoll(int roll, String reason)
{
game.setMovementRoll(roll);
gui.actOnTellMovementRoll(roll, reason);
// kickMoves() now called by kickPhase(), because the kickXXXX
// are now done separately, not implied in the setupXXXXX
// (or in this case, tellMovementRoll()) any more.
// kickMoves();
}
public void tellWhatsHappening(String message)
{
gui.tellWhatsHappening(message);
}
public void kickPhase() // NOT: kickFace! ;-)
{
if (game.isPhase(Phase.SPLIT))
{
kickSplit();
}
else if (game.isPhase(Phase.MOVE))
{
kickMoves();
}
else if (game.isPhase(Phase.FIGHT))
{
kickFight();
}
else if (game.isPhase(Phase.MUSTER))
{
kickMuster();
}
}
private void kickMoves()
{
if (isMyTurn() && autoplay.autoMasterMove() && !game.isGameOver()
&& !replayOngoing)
{
doAutoMoves();
}
}
private void doAutoMoves()
{
boolean again = ai.masterMove();
aiPause();
if (!again)
{
doneWithMoves();
}
}
/** Server sends Client some option setting (e.g. AI type,
* autoPlay for stresstest (also AIs (????), ...)
*/
public void syncOption(String optname, String value)
{
options.setOption(optname, value);
}
// public for IOracle
public int getNumPlayers()
{
return game.getNumPlayers();
}
public void updatePlayerInfo(List<String> infoStrings)
{
if (playersNotInitialized)
{
String searchName = this.owningPlayer.getName();
PlayerClientSide foundPlayer = game.initPlayerInfo(infoStrings,
searchName);
if (!spectator)
{
// returns null if not found, prevent the dummy player
// initialized to it to be overwritten with null
this.owningPlayer = foundPlayer;
}
playersNotInitialized = false;
}
game.updatePlayerInfo(infoStrings);
gui.updateStatusScreen();
}
public boolean canHandleChangedValuesOnlyStyle()
{
// Dummy, only to satisfy the interface
return true;
}
public void updateChangedPlayerValues(String valuesString, String reason)
{
LOGGER.finest("Updating values: " + valuesString + "(reason: "
+ reason + ")");
game.updatePlayerValues(valuesString);
gui.updateStatusScreen();
}
public PlayerClientSide getOwningPlayer()
{
return owningPlayer;
}
// Called by server during load, or by ai or gui when they choose it
public void setColor(PlayerColor color)
{
// this.color = color;
}
public void updateCreatureCount(CreatureType type, int count, int deadCount)
{
getGame().getCaretaker().setAvailableCount(type, count);
getGame().getCaretaker().setDeadCount(type, deadCount);
gui.updateCreatureCountDisplay();
}
void setClosedByServer()
{
closedBy = ClosedByConstant.CLOSED_BY_SERVER;
}
public void disposeClientOriginated()
{
if (disposeInProgress)
{
return;
}
closedBy = ClosedByConstant.CLOSED_BY_CLIENT;
if (connection != null && !connection.isAlreadyDown())
{
// send withdraw, if relevant (not game over or dead already)
// TODO Not in use right now, Server has not enough time to
// handle it before Exception strikes? Need further testing...
// In practice, handling the withDraw would involve writing
// to all clients, and that leads to BrokenPipeException.
// In contrast, disconnect does not send anything to the
// to-be-gone client.
// Solution approach perhaps: pass in disconnect message
// a mode like: withdraw / shortterm (will disconnect soon)
// or longterm (=> play by email) or (CH to sever) "unknown"
// withdrawFromGame();
// SCT will then end the loop and do the dispose.
// So nothing else to do any more here in EDT.
connection.stopSocketClientThread(true);
}
else
{
// SCT already closed and requested to dispose client,
// but user declined. Now, when user manually wants to
// close the board, have to do it directly.
disposeWholeClient();
}
}
// used from server, when game is over and server closes all sockets
public synchronized void disposeClient()
{
if (gotDisposeAlready)
{
return;
}
gotDisposeAlready = true;
disposeWholeClient();
}
// Clean up everything related to _this_ client:
private void disposeWholeClient()
{
gui.handleWebClientRestore();
// -----------------------------------------------
// Now a long decision making, whether to actually close
// everything or not... - depending on the situation.
boolean close = true;
try
{
close = decideWhetherClose();
}
catch (Exception e)
{
LOGGER.log(Level.SEVERE, "Exception " + e.toString()
+ " while deciding whether to close", e);
}
if (close)
{
try
{
disposeInProgress = true;
disposeAll();
gui.setClientInWebClientNull();
}
// just in case, so we are sure to get the unregistering done
catch (Exception e)
{
LOGGER.log(Level.SEVERE,
"During close in client " + owningPlayer.getName()
+ ": got Exception!!!" + e.toString(), e);
}
ViableEntityManager.unregister(this);
}
}
private boolean decideWhetherClose()
{
boolean close = true;
// Defensive: if called too early (owningPlayer not set yet),
// assume it's a human to prevent auto.close.
boolean isAI = (owningPlayer != null && owningPlayer.isAI());
// AIs in general, and any (local or remote) client during
// stresstesting should close without asking...
if (isAI || Options.isStresstest())
{
close = true;
}
else if (closedBy == ClosedByConstant.CLOSED_BY_SERVER)
{
if (isRemote())
{
if (gui.hasBoard())
{
gui.showConnectionClosedMessage();
if (game.isSuspended())
{
close = true;
}
else
{
close = false;
}
}
else
{
gui.actOnGameStartingFailed();
// No board - probably startup failed. Simply silently
close = true;
}
}
else
{
// NOT remote, forced closed: just closing without asking
}
}
else if (closedBy == ClosedByConstant.CLOSED_BY_CLIENT)
{
// ok, explicitly initiated by user.
}
else
{
LOGGER.warning("Client " + getOwningPlayer()
+ ": network connection was unexpectedly closed. "
+ "Trying to reconnect after 1 second ...");
appendToConnectionLog("NOTE: Ooops? Connection with server side "
+ "was unexpectedly closed? "
+ "Will try to reconnect after 1 second ...");
// give it some time...
WhatNextManager.sleepFor(1000);
LOGGER.info("Initiating automatic reconnect!");
fireOneReconnectRunnable(true);
close = false;
}
return close;
}
/* Dispose all windows, and clean up lot of references,
* so that GC can do it's job
* - in case we keep JVM open to play another one...
*/
private void disposeAll()
{
disposeInProgress = true;
connection = null;
server = null;
gui.doCleanupGUI();
}
/* This was earlier done at end of cleanupGUI inside client.
* Now that is moved to GUI class, its called from there after all
* other cleanup has completed (inside the invokeAndWait call).
*/
public void doAdditionalCleanup()
{
this.battleMovement = null;
playersNotInitialized = true;
CustomRecruitBase.resetAllInstances();
}
/** Called from BattleBoard to leave carry mode. */
public void leaveCarryMode()
{
gui.disposePickCarryDialog();
server.leaveCarryMode();
doAutoStrikes();
}
public void doneWithBattleMoves()
{
aiPause();
gui.actOnDoneWithBattleMoves();
server.doneWithBattleMoves();
}
// TODO move to Game or Battle
public List<BattleUnit> getActiveBattleUnits()
{
return getBattleCS().getActiveBattleUnits();
}
private boolean sansLordAutoBattleApplies()
{
if (!game.isBattleOngoing())
{
// Odd: if GUI client tries this when there hasn't been any
// battle, seems EDT hangs and never finishes.
// So, to prevent that do this here only if there is really
// a battle.
return false;
}
if (options.getOption(Options.sansLordAutoBattle))
{
// the option is on. now check for lordly presence...
Legion legion = game.getBattleActiveLegion();
for (Creature creature : legion.getCreatures())
{
CreatureType creatureType = creature.getType();
if (creatureType.isLord())
{
// if any creature in legion is a lord, no auto battle
return false;
}
}
// not returned yet? then no lords. auto battle for this legion
return true;
}
// the option itself is off. sansLordAutoBattles never applies.
return false;
}
// TODO move to Game or Battle
public List<BattleUnit> getInactiveBattleUnits()
{
return getBattleCS().getInactiveBattleUnits();
}
public void aiDoneWithStrikes()
{
aiPause();
doneWithStrikes(true);
}
public void doneWithStrikes(boolean auto)
{
gui.indicateStrikesDone(auto);
server.doneWithStrikes();
}
/** Return true if any strikes were taken. */
private boolean makeForcedStrikes()
{
if (isMyBattlePhase() && autoplay.autoForcedStrike())
{
return strikeMakeForcedStrikes(autoplay.autoRangeSingle());
}
return false;
}
private boolean strikeMakeForcedStrikes(boolean autoRangeSingle)
{
if (getBattlePhase() == null)
{
LOGGER.log(Level.SEVERE,
"Called Strike.makeForcedStrikes() when there is no battle");
return false;
}
else if (!getBattlePhase().isFightPhase() && !isMyBattlePhase())
{
LOGGER.log(Level.SEVERE,
"Called Strike.makeForcedStrikes() in wrong phase");
return false;
}
for (BattleCritter battleUnit : getActiveBattleUnits())
{
if (!battleUnit.hasStruck())
{
Set<BattleHex> set = getBattleCS().findTargets(battleUnit,
autoRangeSingle);
if (set.size() == 1)
{
BattleHex hex = set.iterator().next();
strike(battleUnit.getTag(), hex);
return true;
}
}
}
return false;
}
/** Handle both forced strikes and AI strikes. */
private void doAutoStrikes()
{
if (isMyBattlePhase())
{
if (isAutoplayActive() || sansLordAutoBattleApplies())
{
aiPause();
boolean struck = makeForcedStrikes();
if (!struck)
{
struck = ai.strike(game.getBattleActiveLegion());
}
if (!struck)
{
aiDoneWithStrikes();
}
}
else
{
boolean struck = makeForcedStrikes();
gui.highlightCrittersWithTargets();
// If there are no strikes (or strikeback) to do, be done with
// striking automatically.
// NOTE! This handles also both strike phases of turn 1
// of a battle! So for long delay (roundtrip time) client is
// for a short while "theoretically" in "is supposed to strike"
// status.
if (!struck && findCrittersWithTargets().isEmpty())
{
aiDoneWithStrikes();
}
}
}
}
/**
* Get this legion's info or create if necessary.
*
* TODO move legion creation into a factory on {@link Player}
*/
public LegionClientSide getLegion(String markerId)
{
PlayerClientSide player = (PlayerClientSide)game
.getPlayerByMarkerId(markerId);
LegionClientSide legion = player.getLegionByMarkerId(markerId);
// Added this logging only for the purpose that one gets a clue
// when during the game this happened - the assertion appears only
// on stderr. Now it's also in the log, so one sees what was logged
// just before and after it.
if (legion == null)
{
LOGGER.log(Level.SEVERE, "No legion with markerId '" + markerId
+ "'" + " (for player " + player + "), turn = "
+ getTurnNumber() + " in client " + getOwningPlayer());
}
assert legion != null : "No legion with markerId '" + markerId + "'"
+ " (for player " + player + "), turn = " + getTurnNumber()
+ " in client " + getOwningPlayer();
return legion;
}
/** Remove this eliminated legion, and clean up related stuff. */
public void removeLegion(Legion legion)
{
gui.actOnRemoveLegion(legion);
// TODO Do for all players
if (isMyLegion(legion))
{
getOwningPlayer().addMarkerAvailable(legion.getMarkerId());
}
legion.getPlayer().removeLegion(legion);
gui.alignLegionsMaybe(legion);
}
public int getLegionHeight(String markerId)
{
Legion legionInfo = getLegion(markerId);
if (legionInfo == null)
{
return 0; //no legion, no height
}
return legionInfo.getHeight();
}
/** Needed when loading a game outside split phase. */
public void setLegionStatus(Legion legion, boolean moved,
boolean teleported, EntrySide entrySide, CreatureType lastRecruit)
{
legion.setMoved(moved);
legion.setTeleported(teleported);
legion.setEntrySide(entrySide);
legion.setRecruit(lastRecruit);
}
// TODO make all who use this use directly from game
public List<String> getLegionImageNames(Legion legion)
{
return game.getLegionImageNames(legion);
}
// TODO make all who use this use directly from game
public List<Boolean> getLegionCreatureCertainties(Legion legion)
{
return game.getLegionCreatureCertainties(legion);
}
/**
* Add a new creature to this legion.
*/
public void addCreature(Legion legion, CreatureType creature, String reason)
{
legion.addCreature(creature);
gui.actOnAddCreature(legion, creature, reason);
}
public void removeCreature(Legion legion, CreatureType creature,
String reason)
{
if (legion == null || creature == null)
{
return;
}
gui.actOnRemoveCreature(legion, creature, reason);
int height = legion.getHeight();
legion.removeCreature(creature);
if (height <= 1)
{
// do not remove this, sever will give explicit order to remove it
// removeLegion(markerId);
}
if (height <= 1 && getTurnNumber() == -1)
{
// hack to remove legions correctly during load
removeLegion(legion);
}
gui.actOnRemoveCreaturePart2(legion);
}
/** Reveal creatures in this legion, some of which already may be known.
* - this "reveal" is related to data coming from server being
* revealed to the split prediction
* */
public void revealCreatures(Legion legion,
final List<CreatureType> creatures, String reason)
{
gui.eventViewerRevealCreatures(legion, creatures, reason);
((LegionClientSide)legion).revealCreatures(creatures);
}
/* pass revealed info to SplitPrediction and the GUI
*/
public void revealEngagedCreatures(Legion legion,
final List<CreatureType> names, boolean isAttacker, String reason)
{
revealCreatures(legion, names, reason);
gui.revealEngagedCreatures(legion, names, isAttacker, reason);
}
public void removeDeadBattleChits()
{
for (BattleUnit battleUnit : getBattleCS().getBattleUnits())
{
if (battleUnit.isDead())
{
// Moved it.remove() to a 2nd loop that is then done inside
// Battle, otherwise unsupportedOperationException because
// we get an unmodifiable list from Battle.
// it.remove();
gui.removeBattleChit(battleUnit);
// Also remove it from legion.
battleUnit.getLegion().removeCreature(battleUnit.getType());
// And generate EventViewer event:
// Note that have to do the legion.removeCreature before this
// gui / eventviewer call so that eventViewer sees already new
// height.
gui.eventViewerSetCreatureDead(battleUnit);
}
}
getBattleCS().removeDeadBattleChits();
gui.repaintBattleBoard();
}
/** Create a new BattleUnit and (if GUI) a new GUIBattleChit with
* the given parameters. Place them in given hex,
* and add them to the lists of BattleUnits (in Battle[ClientSide])
* and GUIBattleChits (in GUI)
*/
public void placeNewChit(String bareImageName, boolean inverted, int tag,
BattleHex hex)
{
Legion legion = inverted ? getDefender() : getAttacker();
String imageName = bareImageName;
if (imageName.equals(Constants.titan))
{
imageName = legion.getPlayer().getTitanBasename();
}
else if (imageName.equals(Constants.angel))
{
imageName = legion.getPlayer().getAngelBasename();
}
CreatureType type = getGame().getVariant().getCreatureByName(
bareImageName);
BattleUnit battleUnit = getBattleCS().createBattleUnit(imageName,
inverted, tag, hex, type, legion);
gui.actOnPlaceNewChit(imageName, battleUnit, hex);
}
public CreatureType chooseBestPotentialRecruit(LegionClientSide legion,
MasterHex hex, List<CreatureType> recruits)
{
CreatureType recruit = ai.getVariantRecruitHint(legion, hex, recruits);
return recruit;
}
public IClientGUI getGUI()
{
return gui;
}
public void tellReplay(boolean val, int maxTurn)
{
replayOngoing = val;
gui.actOnTellReplay(maxTurn);
}
public boolean isReplayOngoing()
{
return replayOngoing;
}
public void tellRedo(boolean val)
{
redoOngoing = val;
gui.actOnTellRedoChange();
}
public boolean isRedoOngoing()
{
return redoOngoing;
}
public boolean isReplayBeforeRedo()
{
return replayOngoing && !redoOngoing;
}
public void confirmWhenCaughtUp()
{
server.clientConfirmedCatchup();
}
public void confirmRelayedPeerRequest(String requestingClientName)
{
server.peerRequestProcessed(requestingClientName);
}
public void initBoard()
{
// SyncOptions is now done, so now we can create movement and
// battleMovement which need the options.
/* NOTE/WARNING:
* using option listeners here would have to be done the *right way*,
* because changes from server appear here as string-based,
* thus a listener based on the boolean option would not work.
* For now, we just make sure we create BattleMovement after all
* server-to-client-option-sync'ing is completed.
*/
this.battleMovement = new BattleMovement(game, options);
this.movement = new MovementClientSide(game, options);
LOGGER.finest(getOwningPlayer().getName() + " Client.initBoard()");
ai.setVariant(VariantSupport.getCurrentVariant());
gui.initBoard();
}
public void setEventExecutor(EventExecutor eventExecutor)
{
this.eventExecutor = eventExecutor;
}
public EventExecutor getEventExecutor()
{
return this.eventExecutor;
}
public void setPlayerName(String playerName)
{
this.owningPlayer.setName(playerName);
InstanceTracker.setId(this, "Client " + playerName);
InstanceTracker.setId(ai, "AI: " + playerName);
connection.updatePlayerName(playerName);
}
public void createSummonAngel(Legion legion)
{
SummonInfo summonInfo = new SummonInfo();
List<Legion> possibleDonors = game.findLegionsWithSummonables(legion);
if (possibleDonors.size() < 1)
{
// Should not happen any more since I fixed it on server side.
// But, who knows. Better check earlier than somehwere inside
// the GUI.
LOGGER.warning("Server requested us to createSummonAngel but "
+ "there are no legions with summonable Angels!");
// still, do the summon with the default created summonInfo,
// Server might wait for an answer (so, NOT just return without
// doing anything).
doSummon(summonInfo);
}
else
{
if (autoplay.autoSummonAngels())
{
summonInfo = ai.summonAngel(legion, possibleDonors);
doSummon(summonInfo);
}
else
{
gui.doPickSummonAngel(legion, possibleDonors);
// GUI does a callback (sends it to server itself).
}
}
}
/**
* recruits is the list of acquirables that can be chosen from
* for a certain point value reached. E.g. for getting 180 points,
* going from 380 + 180 = 560,
* game would first call this for 400: recruits = [Angel]
* and then call it once more for 500: recruits = [Angel, Archangel]
*/
public void askAcquireAngel(Legion legion, List<CreatureType> recruits)
{
if (autoplay.autoAcquireAngels())
{
acquireAngelCallback(legion, ai.acquireAngel(legion, recruits));
}
else
{
gui.doAcquireAngel(legion, recruits);
}
}
public void acquireAngelCallback(Legion legion, CreatureType angelType)
{
server.acquireAngel(legion, angelType);
}
/** Present a dialog allowing the player to enter via land or teleport.
* Return true if the player chooses to teleport. */
private boolean chooseWhetherToTeleport(MasterHex hex)
{
if (autoplay.autoMasterMove())
{
return false;
}
// No point in teleporting if entry side is moot.
// (Note that the "what if it's own legion there?" exception as
// in pick-entry-side logic is not needed here - can't teleport
// to same hex.
if (!game.isOccupied(hex))
{
return false;
}
return gui.chooseWhetherToTeleport();
}
/** Allow the player to choose whether to take a penalty (fewer dice
* or higher strike number) in order to be allowed to carry. */
public void askChooseStrikePenalty(List<String> choices)
{
if (isAutoplayActive() || sansLordAutoBattleApplies())
{
String choice = ai.pickStrikePenalty(choices);
assignStrikePenalty(choice);
}
else
{
gui.doPickStrikePenalty(this, choices);
}
}
public void assignStrikePenalty(String prompt)
{
gui.highlightCrittersWithTargets();
server.assignStrikePenalty(prompt);
}
// TODO Move legion markers to slayer on client side.
// TODO parameters should be PlayerState
public void tellPlayerElim(Player deadPlayer, Player slayer)
{
assert deadPlayer != null;
LOGGER.log(Level.FINEST, this.owningPlayer.getName()
+ " tellPlayerElim(" + deadPlayer + ", " + slayer + ")");
// TODO Merge these
// TODO should this be rather calling Player.die()?
deadPlayer.setDead(true);
((PlayerClientSide)deadPlayer).removeAllLegions();
// otherwise called too early, e.g. someone quitted
// already during game start...
if (this.owningPlayer.equals(deadPlayer))
{
playerAlive = false;
}
}
public void tellGameOver(String message, boolean disposeFollows, boolean suspended)
{
LOGGER.info("Client " + getOwningPlayer()
+ " received from server game over message: " + message);
game.setGameOver(true, message);
game.setSuspended(suspended);
gui.actOnTellGameOver(message, disposeFollows, suspended);
}
public void doFight(MasterHex hex)
{
if (!isMyTurn())
{
return;
}
engage(hex);
}
// TODO: handle better (Mock, extend Client class?)
public boolean testCaseAutoDontFlee = false;
public void askConcede(Legion ally, Legion enemy)
{
if (testCaseAutoDontFlee)
{
LOGGER.fine("askConcede: test case auto-answers 'doNotConcede'");
server.doNotConcede(ally);
}
else if (autoplay.autoConcede())
{
answerConcede(ally, ai.concede(ally, enemy));
}
else
{
gui.showConcede(this, ally, enemy);
}
}
public void askFlee(Legion ally, Legion enemy)
{
if (testCaseAutoDontFlee)
{
LOGGER.fine("askFlee: test case auto-answers 'doNotFlee'");
server.doNotFlee(ally);
}
else if (autoplay.autoFlee())
{
if (eventExecutor.getRetriggeredEventOngoing())
{
boolean reply = ai.flee(ally, enemy);
inactivityAutoFleeOrConcede(reply);
}
else
{
answerFlee(ally, ai.flee(ally, enemy));
}
}
else
{
gui.showFlee(this, ally, enemy);
}
}
/**
* Make the concede dialog reply wit the answer the AI has provided,
* as if the user would have selected something; so that the dialog
* is disposed cleanly.
* @param reply Whether to fleed/conced, or not.
*/
public void inactivityAutoFleeOrConcede(boolean reply)
{
gui.inactivityAutoFleeOrConcede(reply);
}
public void answerFlee(Legion ally, boolean answer)
{
if (answer)
{
server.flee(ally);
}
else
{
server.doNotFlee(ally);
}
}
public void answerConcede(Legion legion, boolean answer)
{
if (answer)
{
concede(legion);
}
else
{
doNotConcede(legion);
}
}
// TODO: handle better (Mock, extend Client class?)
public boolean testCaseAutoDenyNegotiate = false;
public void askNegotiate(Legion attacker, Legion defender)
{
if (getAttacker() != attacker)
{
LOGGER
.severe("Attacker in game differs from attacker given in askNegotiate!");
}
if (getDefender() != defender)
{
LOGGER
.severe("Defender in game differs from defender given in askNegotiate!");
}
if (autoplay.autoNegotiate())
{
// XXX AI players just fight for now.
Proposal proposal = new Proposal(getAttacker(), getDefender(),
true, false, null, null);
makeProposal(proposal);
}
else if (testCaseAutoDenyNegotiate)
{
fight(attacker.getCurrentHex());
}
else
{
gui.showNegotiate(getAttacker(), getDefender());
}
}
/** Inform this player about the other player's proposal. */
public void tellProposal(String proposalString)
{
gui.tellProposal(proposalString);
}
/** Called from both Negotiate and ReplyToProposal. */
public void negotiateCallback(Proposal proposal, boolean respawn)
{
if (proposal != null && proposal.isFight())
{
fight(getAttacker().getCurrentHex());
return;
}
else if (proposal != null)
{
makeProposal(proposal);
}
if (respawn)
{
gui.respawnNegotiate();
}
}
private void makeProposal(Proposal proposal)
{
server.makeProposal(proposal.toString());
}
public void tellSlowResults(int targetTag, int slowValue)
{
BattleCritter targetCritter = getBattleCS().getBattleUnit(targetTag);
if (targetCritter != null)
{
if (slowValue != 0)
{
targetCritter.addSlowed(slowValue);
}
}
}
public void tellStrikeResults(int strikerTag, int targetTag,
int strikeNumber, List<String> rolls, int damage, boolean killed,
boolean wasCarry, int carryDamageLeft,
Set<String> carryTargetDescriptions)
{
BattleCritter battleUnit = getBattleCS().getBattleUnit(strikerTag);
if (battleUnit != null)
{
battleUnit.setStruck(true);
}
gui.disposePickCarryDialog();
BattleUnit targetUnit = getBattleCS().getBattleUnit(targetTag);
BattleCritter targetCritter = getBattleCS().getBattleUnit(targetTag);
gui.actOnTellStrikeResults(wasCarry, strikeNumber, rolls, battleUnit,
targetUnit);
if (targetCritter != null)
{
if (killed)
{
targetCritter.setDead(true);
}
else
{
if (damage != 0) // Can be negative if creature is being healed
{
targetCritter.setHits(targetUnit.getHits() + damage);
gui.actOnHitsSet(targetUnit);
}
}
}
if (strikerTag == Constants.HEX_DAMAGE)
{
// Do not trigger auto strikes in parallel with setupBattleFight()
}
else if (carryDamageLeft >= 1 && !carryTargetDescriptions.isEmpty())
{
pickCarries(carryDamageLeft, carryTargetDescriptions);
}
else
{
doAutoStrikes();
}
}
public void nak(String reason, String errmsg)
{
LOGGER.log(Level.WARNING, owningPlayer.getName() + " got nak for "
+ reason + " " + errmsg);
if (reason.startsWith("doSplit"))
{
LOGGER.warning("NAK: " + reason);
String available = Glob.glob(",",
owningPlayer.getMarkersAvailable());
String notAvail = Glob.glob(",", owningPlayer.getMarkersUsed());
LOGGER.warning("Available: " + available + ": not available "
+ notAvail);
}
else
{
LOGGER.warning("other NAK,reason: " + reason);
}
recoverFromNak(reason, errmsg);
}
private void recoverFromNak(String reason, String errmsg)
{
LOGGER.log(Level.FINEST, owningPlayer.getName() + " recoverFromNak "
+ reason + " " + errmsg);
if (reason == null)
{
LOGGER.log(Level.SEVERE, "recoverFromNak with null reason!");
}
else if (reason.equals(Constants.doSplit))
{
showMessageDialog(errmsg);
// if AI just got NAK for split, there's no point for
// kickSplit again. Instead, let's just be DoneWithSplits.
if (isMyTurn() && autoplay.autoSplit() && !game.isGameOver())
{
// XXX This may cause advancePhance illegally messages,
// if e.g. SimpleAI fires two splits, both gets rejected,
// and it responds with dineWithSplits two times.
// But this whole situation should normally never happen, did
// happen now only because of server/client data regarding
// available markers was out of sync and then as reaction
// to nak for didSplit do kickSplit() did just end in an
// endless loop. Now perhaps 2 error messages, but no hang.
doneWithSplits();
}
}
else if (reason.equals(Constants.doneWithSplits))
{
showMessageDialog(errmsg);
kickSplit();
}
else if (reason.equals(Constants.doMove))
{
gui.actOnMoveNak();
showMessageDialog(errmsg);
kickMoves();
}
else if (reason.equals(Constants.doneWithMoves))
{
showMessageDialog(errmsg);
kickMoves();
}
else if (reason.equals(Constants.doBattleMove))
{
handleFailedBattleMove(errmsg);
}
else if (reason.equals(Constants.doneWithBattleMoves))
{
// TODO why can we ignore this?
/*
* Clemens' guess: This can not really happen based on "user did
* something wrong"; when this happened, it was because user
* clicked twice (due to delayed response from server)
* => the illegal nak was because it was already different phase.
* So, no point to bother user with that.
* With recent (12.10.2015) changes (where user gets visual
* feedback that Done was clicked) this situation "should"
* not happen any more...
*/
}
else if (reason.equals(Constants.assignStrikePenalty))
{
doAutoStrikes();
}
else if (reason.equals(Constants.strike))
{
doAutoStrikes();
}
else if (reason.equals(Constants.doneWithStrikes))
{
showMessageDialog(errmsg);
gui.highlightCrittersWithTargets();
gui.revertDoneIndicator();
}
else if (reason.equals(Constants.doneWithEngagements))
{
showMessageDialog(errmsg);
}
else if (reason.equals(Constants.doRecruit))
{
// TODO: earlier here was nothing, but a TODO "why can we ignore
// Did/does the adding showMessageDialog cause problems?"
showMessageDialog(errmsg);
}
else if (reason.equals(Constants.doneWithRecruits))
{
// TODO why can we ignore this?
}
else
{
LOGGER.log(Level.WARNING, owningPlayer.getName()
+ " unexpected nak " + reason + " " + errmsg);
}
}
private void pickCarries(int carryDamage,
Set<String> carryTargetDescriptions)
{
if (!isMyBattlePhase())
{
return;
}
if (carryDamage < 1 || carryTargetDescriptions.isEmpty())
{
leaveCarryMode();
}
else if (carryTargetDescriptions.size() == 1
&& autoplay.autoCarrySingle())
{
Iterator<String> it = carryTargetDescriptions.iterator();
String desc = it.next();
String targetHexLabel = desc.substring(desc.length() - 2);
BattleHex targetHex = game.getBattleSite().getTerrain()
.getHexByLabel(targetHexLabel);
applyCarries(targetHex);
}
else
{
if (isAutoplayActive() || sansLordAutoBattleApplies())
{
aiPause();
ai.handleCarries(carryDamage, carryTargetDescriptions);
}
else
{
gui.doPickCarries(this, carryDamage, carryTargetDescriptions);
}
}
}
public void initBattle(MasterHex hex, int battleTurnNumber,
Player battleActivePlayer, BattlePhase battlePhase, Legion attacker,
Legion defender)
{
gui.cleanupNegotiationDialogs();
game.initBattle(hex, battleTurnNumber, battleActivePlayer,
battlePhase, attacker, defender);
if (isAutoplayActive() || sansLordAutoBattleApplies())
{
ai.initBattle();
}
gui.actOnInitBattle();
}
public void messageFromServer(String message)
{
gui.showMessageDialogAndWait(message);
}
public void showMessageDialog(String message)
{
gui.showMessageDialogAndWait(message);
}
public void cleanupBattle()
{
LOGGER.log(Level.FINEST, owningPlayer.getName()
+ " Client.cleanupBattle()");
gui.actOnCleanupBattle();
if (isAutoplayActive() || sansLordAutoBattleApplies())
{
ai.cleanupBattle();
}
game.cleanupBattle();
}
public boolean canRecruit(Legion legion)
{
return legion.hasMoved() && legion.getHeight() < 7
&& !legion.hasRecruited() && !legion.getPlayer().isDead()
&& !findEligibleRecruits(legion, legion.getCurrentHex()).isEmpty();
}
/** Used for human players only. */
public void doRecruit(Legion legion)
{
if (legion == null || !isMyTurn() || !isMyLegion(legion))
{
// TODO is it good to return quietly here? It seems the method should
// not have been called in the first place
return;
}
if (legion.hasRecruited() || legion.getSkipThisTime())
{
gui.undoRecruit(legion);
return;
}
if (!canRecruit(legion))
{
// TODO is it good to return quietly here? It seems the method should
// not have been called in the first place
return;
}
String hexDescription = legion.getCurrentHex().getDescription();
CreatureType recruit = gui.doPickRecruit(legion, hexDescription);
if (legion.getSkipThisTime())
{
// TODO handle better, see ClientGUI.markLegionAsSkipRecruit
return;
}
if (recruit == null)
{
return;
}
String recruiterName = findRecruiterName(legion, recruit,
hexDescription);
if (recruiterName == null)
{
return;
}
doRecruit(legion, recruit.getName(), recruiterName);
}
// TODO use CreatureType instead of String
public void doRecruit(Legion legion, String recruitName,
String recruiterName)
{
CreatureType recruited = (recruitName == null) ? null : game
.getVariant().getCreatureByName(recruitName);
// TODO solve this better?
if ("none".equals(recruiterName))
{
recruiterName = null;
}
CreatureType recruiter = (recruiterName == null) ? null : game
.getVariant().getCreatureByName(recruiterName);
// Call server even if some arguments are null, to get past
// reinforcement.
server.doRecruit(new Recruitment(legion, recruited, recruiter));
}
/** Always needs to call server.doRecruit(), even if no recruit is
* wanted, to get past the reinforcing phase. */
public void doReinforce(Legion legion)
{
if (autoplay.autoReinforce())
{
ai.reinforce(legion);
}
else
{
String hexDescription = legion.getCurrentHex().getDescription();
CreatureType recruit = gui.doPickRecruit(legion, hexDescription);
String recruiterName = null;
if (recruit != null)
{
recruiterName = findRecruiterName(legion, recruit,
hexDescription);
}
doRecruit(legion, (recruit == null) ? null : recruit.getName(),
recruiterName);
}
}
public void didRecruit(Legion legion, CreatureType recruit,
CreatureType recruiter, int numRecruiters)
{
List<CreatureType> recruiters = new ArrayList<CreatureType>();
if (numRecruiters >= 1 && recruiter != null)
{
for (int i = 0; i < numRecruiters; i++)
{
recruiters.add(recruiter);
}
revealCreatures(legion, recruiters, Constants.reasonRecruiter);
}
String reason = (getBattleSite() != null ? Constants.reasonReinforced
: Constants.reasonRecruited);
addCreature(legion, recruit, reason);
legion.setRecruit(recruit);
if (redoOngoing || !replayOngoing)
{
gui.actOnDidRecruit(legion, recruit, recruiters, reason);
}
}
public void undoRecruit(Legion legion)
{
server.undoRecruit(legion);
}
public void undidRecruit(Legion legion, CreatureType recruit)
{
boolean wasReinforcement;
if (game.isBattleOngoing())
{
wasReinforcement = true;
gui.eventViewerCancelReinforcement(recruit, getTurnNumber());
}
else
{
// normal undoRecruit
wasReinforcement = false;
legion.removeCreature(recruit);
}
legion.setRecruit(null);
if (!isReplayOngoing() || isRedoOngoing())
{
gui.actOnUndidRecruitPart(legion, wasReinforcement,
getTurnNumber());
}
}
public void doneWithRecruits()
{
if (!isMyTurn())
{
return;
}
aiPause();
server.doneWithRecruits();
}
/** null means cancel. "none" means no recruiter (tower creature). */
private String findRecruiterName(Legion legion, CreatureType recruit,
String hexDescription)
{
String recruiterName = null;
List<String> recruiters = findEligibleRecruiters(legion, recruit);
int numEligibleRecruiters = recruiters.size();
if (numEligibleRecruiters == 0)
{
// A warm body recruits in a tower.
recruiterName = "none";
}
else if (autoplay.autoPickRecruiter() || numEligibleRecruiters == 1)
{
// If there's only one possible recruiter, or if
// the user has chosen the autoPickRecruiter option,
// then just reveal the first possible recruiter.
recruiterName = recruiters.get(0);
}
else
{
recruiterName = gui.doPickRecruiter(recruiters, hexDescription,
legion);
}
return recruiterName;
}
// TODO move to GameClientSide
private void resetLegionMovesAndRecruitData()
{
for (Player player : game.getPlayers())
{
for (Legion legion : player.getLegions())
{
legion.setMoved(false);
legion.setTeleported(false);
legion.setRecruit(null);
}
}
}
public void setBoardActive(boolean val)
{
gui.setBoardActive(val);
}
/**
* Called by server when activePlayer changes
*/
public void setupTurnState(Player activePlayer, int turnNumber)
{
// "turn state is first time initialized" means also the game setup
// is completed. GUI might now e.g. enable game saving menu actions.
if (game.isTurnStateStillUninitialized())
{
gui.actOnGameStarting();
}
game.setActivePlayer(activePlayer);
game.setTurnNumber(turnNumber);
gui.actOnTurnOrPlayerChange(this, turnNumber, game.getActivePlayer());
}
/* Quick way to disable a code block; for simple "if (false)"
* Eclipse gives dead code warnings :-(
*/
private boolean isTrue(boolean val)
{
return val;
}
public void prn(String text)
{
if (isTrue(false))
{
return;
}
System.out.println(text);
}
public void setupSplit(Player activePlayer, int turnNumber)
{
resetLegionMovesAndRecruitData();
// Now the actual setup split stuff
game.setPhase(Phase.SPLIT);
numSplitsThisTurn = 0;
gui.actOnSetupSplit();
// kickSplit() now called by kickPhase(), because the kickXXXX
// are now done separately, not implied in the setupXXXXX any more.
// kickSplit();
}
private void kickSplit()
{
if (isMyTurn() && autoplay.autoSplit() && !game.isGameOver())
{
boolean done = ai.split();
if (done)
{
doneWithSplits();
}
}
}
public void setupMove()
{
game.setPhase(Phase.MOVE);
gui.actOnSetupMove();
}
public void setupFight()
{
game.setPhase(Phase.FIGHT);
gui.actOnSetupFight();
// kickFight() now called by kickPhase(), because the kickXXXX
// are now done separately, not implied in the setupXXXXX any more.
// kickFight();
}
// TODO very similar to nextEngagement, even called redundantly...
private void kickFight()
{
// TODO
// In practice we should not get the kickXXXX from server any more
// if it's not our turn... but this below is how it was before
// Should not be needed, there comes a nextEngagement additionally anyway?
/*
if (isMyTurn())
{
gui.defaultCursor();
if (autoplay.autoPickEngagements())
{
aiPause();
ai.pickEngagement();
}
else
{
if (game.findEngagements().isEmpty())
{
doneWithEngagements();
}
}
}
else
{
LOGGER
.warning("Got called kickFight but it's not our phase? Client "
+ getOwningPlayer().getName()
+ ", active player "
+ getActivePlayer().getName());
}
*/
}
// TODO check overlap with kickFight()
public void nextEngagement()
{
gui.highlightEngagements();
if (isMyTurn())
{
// TODO search eng. first, decide then based on autoXXX
if (autoplay.autoPickEngagements())
{
aiPause();
MasterHex hex = ai.pickEngagement();
if (hex != null)
{
engage(hex);
}
else
{
doneWithEngagements();
}
}
else
{
gui.defaultCursor();
}
}
}
/* TODO the whole Done with Engagements is nowadays probably,
* obsolete, server does advancePhase automatically.
* Check also GUI classes accordingly!
*/
public void doneWithEngagements()
{
if (!isMyTurn())
{
return;
}
aiPause();
server.doneWithEngagements();
}
public void setupMuster()
{
game.setPhase(Phase.MUSTER);
gui.actOnSetupMuster();
// Not here any more...
// kickMuster();
}
private void kickMuster()
{
if (game.isPhase(Phase.MUSTER) && isMyTurn() && isAlive()
&& !isReplayOngoing())
{
if (noRecruitActionPossible())
{
doneWithRecruits();
}
else if (autoplay.autoRecruit())
{
// Note that this fires all doRecruit calls in one row,
// i.e. does NOT wait for callback from server.
ai.muster();
// For autoRecruit alone, do not automatically say we are done.
// Allow humans to override. But full autoPlay be done.
if (isAutoplayActive())
//|| sansLordAutoBattleApplies())
{
doneWithRecruits();
}
}
}
}
public void setupBattleSummon(Player battleActivePlayer,
int battleTurnNumber)
{
getBattleCS().setupPhase(BattlePhase.SUMMON, battleActivePlayer,
battleTurnNumber);
gui.actOnSetupBattleSummon();
}
public void setupBattleRecruit(Player battleActivePlayer,
int battleTurnNumber)
{
getBattleCS().setupPhase(BattlePhase.RECRUIT, battleActivePlayer,
battleTurnNumber);
gui.actOnSetupBattleRecruit();
}
public void setupBattleMove(Player battleActivePlayer, int battleTurnNumber)
{
// TODO clean up order of stuff here
getBattleCS().setBattleActivePlayer(battleActivePlayer);
getBattleCS().setBattleTurnNumber(battleTurnNumber);
// Just in case the other player started the battle
// really quickly.
gui.cleanupNegotiationDialogs();
getBattleCS().resetAllBattleMoves();
getBattleCS().setBattlePhase(BattlePhase.MOVE);
gui.actOnSetupBattleMove();
if (isMyBattlePhase()
&& (isAutoplayActive() || sansLordAutoBattleApplies()))
{
bestMoveOrder = ai.battleMove();
failedBattleMoves = new ArrayList<CritterMove>();
kickBattleMove();
}
}
private void kickBattleMove()
{
if (bestMoveOrder == null || bestMoveOrder.isEmpty())
{
if (failedBattleMoves == null || failedBattleMoves.isEmpty())
{
doneWithBattleMoves();
}
else
{
retryFailedBattleMoves();
}
}
else
{
CritterMove cm = bestMoveOrder.get(0);
tryBattleMove(cm);
}
}
public void tryBattleMove(CritterMove cm)
{
BattleCritter critter = cm.getCritter();
BattleHex hex = cm.getEndingHex();
doBattleMove(critter.getTag(), hex);
aiPause();
}
private void retryFailedBattleMoves()
{
bestMoveOrder = failedBattleMoves;
failedBattleMoves = null;
ai.retryFailedBattleMoves(bestMoveOrder);
kickBattleMove();
}
public BattleClientSide getBattleCS()
{
return game.getBattleCS();
}
/** Used for both strike and strikeback. */
public void setupBattleFight(BattlePhase battlePhase,
Player battleActivePlayer)
{
getBattleCS().setupBattleFight(battlePhase, battleActivePlayer);
gui.actOnSetupBattleFight();
doAutoStrikes();
}
/** Create marker if necessary, and place it in hexLabel. */
public void tellLegionLocation(Legion legion, MasterHex hex)
{
legion.setCurrentHex(hex);
gui.actOnTellLegionLocation(legion, hex);
}
public PlayerColor getColor()
{
return getOwningPlayer().getColor();
}
public String getShortColor()
{
return getColor().getShortName();
}
public Player getBattleActivePlayer()
{
return game.getBattleActivePlayer();
}
public Engagement getEngagement()
{
return game.getEngagement();
}
public boolean isEngagementStartupOngoing()
{
return engagementStartupOngoing;
}
// public for IOracle
// TODO placeholder, move at some point fully to Game ?
public Legion getDefender()
{
return game.getEngagement().getDefendingLegion();
}
// public for IOracle
// TODO placeholder, move at some point fully to Game ?
public Legion getAttacker()
{
return game.getEngagement().getAttackingLegion();
}
// public for IOracle
public MasterHex getBattleSite()
{
return game.getBattleSite();
}
public BattlePhase getBattlePhase()
{
return game.getBattlePhase();
}
// public for IOracle and BattleBoard
public int getBattleTurnNumber()
{
return game.getBattleTurnNumber();
}
public void doBattleMove(int tag, BattleHex hex)
{
server.doBattleMove(tag, hex);
}
public void undoBattleMove(BattleHex hex)
{
server.undoBattleMove(hex);
}
private void markBattleMoveSuccessful(int tag, BattleHex endingHex)
{
if (bestMoveOrder != null)
{
Iterator<CritterMove> it = bestMoveOrder.iterator();
while (it.hasNext())
{
CritterMove cm = it.next();
if (tag == cm.getTag() && endingHex.equals(cm.getEndingHex()))
{
// Remove this CritterMove from the list to show
// that it doesn't need to be retried.
it.remove();
}
}
}
kickBattleMove();
}
private void handleFailedBattleMove(String errmsg)
{
LOGGER.log(Level.FINEST, owningPlayer.getName()
+ "handleFailedBattleMove");
if (isAutoplayActive() || sansLordAutoBattleApplies())
{
if (bestMoveOrder != null)
{
Iterator<CritterMove> it = bestMoveOrder.iterator();
if (it.hasNext())
{
CritterMove cm = it.next();
it.remove();
if (failedBattleMoves != null)
{
failedBattleMoves.add(cm);
}
}
}
kickBattleMove();
}
else
{
gui.showMessageDialogAndWait(errmsg);
}
gui.actOnPendingBattleMoveOver();
}
public void tellBattleMove(int tag, BattleHex startingHex,
BattleHex endingHex, boolean undo)
{
boolean rememberForUndo = false;
boolean isMyCritter = owningPlayer.equals(game.getPlayerByTag(tag));
if (isMyCritter && !undo)
{
rememberForUndo = true;
if (isAutoplayActive() || sansLordAutoBattleApplies())
{
markBattleMoveSuccessful(tag, endingHex);
}
}
BattleCritter battleUnit = getBattleCS().getBattleUnit(tag);
if (battleUnit != null)
{
battleUnit.setCurrentHex(endingHex);
battleUnit.setMoved(!undo);
}
gui.actOnTellBattleMove(startingHex, endingHex, rememberForUndo);
}
/** Attempt to have critter tag strike the critter in hex. */
public void strike(int tag, BattleHex hex)
{
gui.resetStrikeNumbers();
server.strike(tag, hex);
}
/** Attempt to apply carries to the critter in hex. */
public void applyCarries(BattleHex hex)
{
server.applyCarries(hex);
gui.actOnApplyCarries(hex);
}
public boolean isInContact(BattleCritter critter, boolean countDead)
{
return game.getBattleCS().isInContact(critter, countDead);
}
// TODO move to Battle
/** Return a set of hexLabels. */
public Set<BattleHex> findMobileCritterHexes()
{
Set<BattleHex> set = new HashSet<BattleHex>();
for (BattleCritter critter : getActiveBattleUnits())
{
if (!critter.hasMoved() && !isInContact(critter, false))
{
set.add(critter.getCurrentHex());
}
}
return set;
}
// TODO move to Battle
/** Return a set of BattleUnits. */
public Set<BattleUnit> findMobileBattleUnits()
{
Set<BattleUnit> set = new HashSet<BattleUnit>();
for (BattleUnit battleUnit : getActiveBattleUnits())
{
if (!battleUnit.hasMoved() && !isInContact(battleUnit, false))
{
set.add(battleUnit);
}
}
return set;
}
public Set<BattleHex> showBattleMoves(BattleCritter battleCritter)
{
return battleMovement.showMoves(battleCritter);
}
// TODO move to Battle
public Set<BattleHex> findCrittersWithTargets()
{
return getBattleCS().findCrittersWithTargets(this);
}
// TODO move to Battle
public Set<BattleHex> findStrikes(int tag)
{
return getBattleCS().findTargets(tag);
}
// Mostly for SocketClientThread
public Player getPlayerByName(String name)
{
return game.getPlayerByName(name);
}
// TODO active or not would probably work better as state in PlayerState
// NOTTODO ... I'd say it's not about "is one active or not" but which one
// is the one... ?
public Player getActivePlayer()
{
return game.getActivePlayer();
}
public Phase getPhase()
{
return game.getPhase();
}
// public for IOracle
public int getTurnNumber()
{
return game.getTurnNumber();
}
public int getNumSplitsThisTurn()
{
return numSplitsThisTurn;
}
public void askPickColor(List<PlayerColor> colorsLeft)
{
if (autoplay.autoPickColor())
{
// Convert favorite colors from a comma-separated string to a list.
String favorites = options.getStringOption(Options.favoriteColors);
List<PlayerColor> favoriteColors = null;
if (favorites != null)
{
favoriteColors = PlayerColor.getByName(Split.split(',',
favorites));
}
else
{
favoriteColors = new ArrayList<PlayerColor>();
}
PlayerColor color = ai.pickColor(colorsLeft, favoriteColors);
answerPickColor(color);
}
else
{
// calls answerPickColor
gui.doPickColor(owningPlayer.getName(), colorsLeft);
}
}
public void answerPickColor(PlayerColor color)
{
setColor(color);
server.assignColor(color);
}
public void askPickFirstMarker()
{
Set<String> markersAvailable = getOwningPlayer().getMarkersAvailable();
if (autoplay.autoPickMarker())
{
String markerId = ai.pickMarker(markersAvailable,
getOwningPlayer().getShortColor());
assignFirstMarker(markerId);
}
else
{
gui.doPickInitialMarker(markersAvailable);
}
}
public void assignFirstMarker(String markerId)
{
server.assignFirstMarker(markerId);
}
public void log(String message)
{
LOGGER.log(Level.INFO, message);
}
/**
*
* Called by MasterBoard.actOnLegion() when human user clicked on a
* legion to split it. This method here then:
* Verifies that splitting is legal and possible at all;
* Then get a child marker selected (either by dialog, or if
* autoPickMarker set, ask AI to pick one);
* If childMarkerId selection was not canceled (returned non-null),
* bring up the split dialog (which creatures go into which legion);
* and if that returns a list (not null) then call doSplit(...,...,...)
* which sends the request to server.
*
* @param parent The legion selected to split
*/
public void doSplit(Legion parent)
{
LOGGER.log(Level.FINER,
"Client.doSplit, marker=" + parent.getMarkerId());
if (!isMyTurn())
{
LOGGER.log(Level.SEVERE, "Not my turn!");
// TODO I think this is useless here
kickSplit();
return;
}
// Can't split other players' legions.
if (!isMyLegion(parent))
{
LOGGER.log(Level.SEVERE, "Not my legion!");
// TODO is this needed here?
kickSplit();
return;
}
Set<String> markersAvailable = getOwningPlayer().getMarkersAvailable();
// Need a legion marker to split.
if (markersAvailable.size() < 1)
{
LOGGER.finer("no legion markers");
gui.showMessageDialogAndWait("No legion markers");
// TODO is this useful here?
kickSplit();
return;
}
LOGGER.finest("Legion markers: " + markersAvailable.size());
if (parent.getSplitRequestSent())
{
LOGGER.finer("doSplit(): split request pending");
gui.showMessageDialogAndWait("Split request still pending\n(waiting for server response)");
// TODO is this useful here?
kickSplit();
return;
}
if (parent.getUndoSplitRequestSent())
{
LOGGER.finer("doSplit(): undo split request pending");
gui.showMessageDialogAndWait("Undo-split request still pending\n(waiting for server response)");
// TODO is this useful here?
kickSplit();
return;
}
// Legion must be tall enough to split.
if (parent.getHeight() < 4)
{
gui.showMessageDialogAndWait("Legion is too short to split");
kickSplit();
return;
}
// Enforce only one split on turn 1.
if (getTurnNumber() == 1 && numSplitsThisTurn > 0)
{
gui.showMessageDialogAndWait("Can only split once on the first turn");
kickSplit();
return;
}
String childId = null;
if (autoplay.autoPickMarker())
{
childId = ai.pickMarker(markersAvailable, getOwningPlayer()
.getShortColor());
doTheSplitting(parent, childId);
}
else
{
gui.doPickSplitMarker(parent, markersAvailable);
}
}
public void doTheSplitting(Legion parent, String childId)
{
if (childId != null)
{
List<CreatureType> crestures = gui.doPickSplitLegion(parent,
childId);
if (crestures != null)
{
sendDoSplitToServer(parent, childId, crestures);
gui.actOnSplitRelatedRequestSent();
}
}
}
/** Called by AI and by doSplit() */
public void sendDoSplitToServer(Legion parent, String childMarkerId,
List<CreatureType> creatures)
{
LOGGER.log(Level.FINER,
"Client.doSplit: parent='" + parent.getMarkerId() + "', child='"
+ childMarkerId + "', splitoffs=" + Glob.glob(",", creatures));
parent.setSplitRequestSent(true);
server.doSplit(parent, childMarkerId, creatures);
}
public Set<MasterHex> findPendingSplitHexes()
{
Set<MasterHex> hexes = new HashSet<MasterHex>();
for (Legion l : getOwningPlayer().getPendingSplitLegions())
{
hexes.add(l.getCurrentHex());
}
return hexes;
}
public Set<MasterHex> findPendingUndoSplitHexes()
{
Set<MasterHex> hexes = new HashSet<MasterHex>();
HashSet<Legion> legions = getOwningPlayer()
.getPendingUndoSplitLegions();
for (Legion l : legions)
{
hexes.add(l.getCurrentHex());
}
return hexes;
}
/**
* Callback from server after any successful split.
*
* TODO childHeight is probably redundant now that we pass the legion object
*/
public void didSplit(MasterHex hex, Legion parent, Legion child,
int childHeight, List<CreatureType> splitoffs, int turn)
{
LOGGER.log(Level.FINEST, "Client.didSplit " + hex + " " + parent + " "
+ child + " " + childHeight + " " + turn);
((LegionClientSide)parent).split(childHeight, child, turn);
child.setCurrentHex(hex);
if (isMyLegion(child))
{
parent.setSplitRequestSent(false);
numSplitsThisTurn++;
getOwningPlayer().removeMarkerAvailable(child.getMarkerId());
}
gui.actOnDidSplit(turn, parent, child, hex);
// check also for phase, because delayed callbacks could come
// after our phase is over but activePlayerName not updated yet.
if (isMyTurn() && game.isPhase(Phase.SPLIT) && !replayOngoing
&& autoplay.autoSplit() && !game.isGameOver())
{
boolean done = ai.splitCallback(parent, child);
if (done)
{
doneWithSplits();
}
}
}
// because of synchronization issues we need to
// be able to pass an undo split request to the server even if it is not
// yet in the client UndoStack
public void undoSplit(Legion splitoff)
{
server.undoSplit(splitoff);
splitoff.setUndoSplitRequestSent(true);
LOGGER.log(Level.FINEST, "called server.undoSplit");
}
public void undidSplit(Legion splitoff, Legion survivor, int turn)
{
((LegionClientSide)survivor).merge(splitoff);
removeLegion(splitoff);
survivor.getPlayer().addMarkerAvailable(splitoff.getMarkerId());
// do the eventViewer stuff before the board, so we are sure to get
// a repaint.
if (!replayOngoing || redoOngoing)
{
gui.eventViewerUndoEvent(splitoff, survivor, turn);
}
if (isMyTurn())
{
numSplitsThisTurn--;
survivor.setUndoSplitRequestSent(false);
}
gui.actOnUndidSplit(survivor, turn);
if (isMyTurn() && game.isPhase(Phase.SPLIT) && !replayOngoing
&& autoplay.autoSplit() && !game.isGameOver())
{
boolean done = ai.splitCallback(null, null);
if (done)
{
doneWithSplits();
}
}
}
public void doneWithSplits()
{
if (!isMyTurn())
{
return;
}
server.doneWithSplits();
gui.actOnDoneWithSplits();
}
private CreatureType figureTeleportingLord(Legion legion, MasterHex hex)
{
List<CreatureType> lords = listTeleportingLords(legion, hex);
switch (lords.size())
{
case 0:
assert false : "We should have at least one teleporting lord";
return null;
case 1:
return lords.get(0);
default:
if (autoplay.autoPickLord())
{
return lords.get(0);
}
else
{
return gui.doPickLord(lords);
}
}
}
/**
* List the lords eligible to teleport this legion to hexLabel.
*/
private List<CreatureType> listTeleportingLords(Legion legion,
MasterHex hex)
{
// Needs to be a List not a Set so that it can be passed as
// an imageList.
List<CreatureType> lords = new ArrayList<CreatureType>();
// Titan teleport
List<Legion> legions = getGame().getLegionsByHex(hex);
if (!legions.isEmpty())
{
Legion legion0 = legions.get(0);
if (legion0 != null && !isMyLegion(legion0))
{
for (Creature creature : legion.getCreatures())
{
if (creature.getType().isTitan())
{
lords.add(creature.getType());
}
}
}
}
// Tower teleport
else
{
for (Creature creature : legion.getCreatures())
{
CreatureType creatureType = creature.getType();
if (creatureType != null && creatureType.isLord()
&& !lords.contains(creatureType))
{
lords.add(creatureType);
}
}
}
return lords;
}
/** If the move looks legal, forward it to server and return true;
* otherwise returns false.
* Also let user or AI pick teleporting Lord and/or entry side,
* if relevant.
*/
public boolean doMove(Legion mover, MasterHex hex)
{
if (mover == null)
{
return false;
}
// This check was earlier after picking entry side. Logically
// it makes more sense to check it first, instead of first let user
// pick something and reject move then still.
// However technically it's irrelevant, since the "pick target hex"
// logic does not even offer same hex if there is another friendly
// legion (= after a split)
// Doing the check here still anyway, since in Infinite e.g. for some
// hexes rolling a 6 allows in theory two entrysides, and to catch
// that (prevent it from asking user to pick entry side), need the
// number of friendly legions value already early.
// if this hex is already occupied, return false
int friendlyLegions = game.getFriendlyLegions(hex, getActivePlayer())
.size();
if (hex.equals(mover.getCurrentHex()))
{
// same hex as starting hex, but it might be occupied by
// multiple legions after split
if (friendlyLegions > 1)
{
return false;
}
}
else
{
if (friendlyLegions > 0)
{
return false;
}
}
boolean teleport = false;
Set<MasterHex> teleports = listTeleportMoves(mover);
Set<MasterHex> normals = listNormalMoves(mover);
if (teleports.contains(hex) && normals.contains(hex))
{
teleport = chooseWhetherToTeleport(hex);
}
else if (teleports.contains(hex))
{
teleport = true;
}
else if (normals.contains(hex))
{
teleport = false;
}
else
{
return false;
}
EntrySide entrySide = null;
Set<EntrySide> entrySides = movement.listPossibleEntrySides(mover,
hex, teleport);
if (entrySides.isEmpty())
{
LOGGER.warning("Attempted move to " + hex
+ " but entrySides is empty?");
return false;
}
else if (!game.isOccupied(hex))
{
// If unoccupied it does not really matter, just take one.
entrySide = entrySides.iterator().next();
}
else if (friendlyLegions == 1 && hex.equals(mover.getCurrentHex()))
{
// it's only we-self there, e.g. on a six in infinite returning
// to same hex on two possible ways.
// dito: entry side does not really matter, just take one.
entrySide = entrySides.iterator().next();
}
else if (autoplay.autoPickEntrySide())
{
entrySide = ai.pickEntrySide(hex, mover, entrySides);
}
else
{
entrySide = gui.doPickEntrySide(hex, entrySides);
if (entrySide == null)
{
return false;
}
}
CreatureType teleportingLord = null;
if (teleport)
{
teleportingLord = figureTeleportingLord(mover, hex);
if (teleportingLord == null)
{
return false;
}
}
// if disconnected, prevent this one, since it would mess up things
// (e.g. pending move tracking)
if (ensureThatConnected())
{
gui.setMovePending(mover, mover.getCurrentHex(), hex);
server.doMove(mover, hex, entrySide, teleport, teleportingLord);
}
return true;
}
public void didMove(Legion legion, MasterHex startingHex,
MasterHex currentHex, EntrySide entrySide, boolean teleport,
CreatureType teleportingLord, boolean splitLegionHasForcedMove)
{
legion.setCurrentHex(currentHex);
legion.setMoved(true);
legion.setEntrySide(entrySide);
legion.setTeleported(teleport);
if (isReplayBeforeRedo())
{
return;
}
gui.actOnDidMove(legion, startingHex, currentHex, teleport,
teleportingLord, splitLegionHasForcedMove);
kickMoves();
}
public void undoMove(Legion legion)
{
server.undoMove(legion);
}
public void undidMove(Legion legion, MasterHex formerHex,
MasterHex currentHex, boolean splitLegionHasForcedMove)
{
legion.setRecruit(null);
legion.setCurrentHex(currentHex);
legion.setMoved(false);
boolean didTeleport = legion.hasTeleported();
legion.setTeleported(false);
gui.actOnUndidMove(legion, formerHex, currentHex,
splitLegionHasForcedMove, didTeleport);
}
public void doneWithMoves()
{
if (!isMyTurn())
{
return;
}
aiPause();
gui.actOnDoneWithMoves();
server.doneWithMoves();
}
public void relocateLegion(Legion legion, MasterHex destination)
{
localServer.getGame().editModeRelocateLegion(legion.getMarkerId(),
destination.getLabel());
}
/** Legion target summons unit from Legion donor.
* @param summonInfo A SummonInfo object that contains the values
* for target, donor and unit.
*/
public void doSummon(SummonInfo summonInfo)
{
assert summonInfo != null : "SummonInfo object must not be null!";
if (summonInfo.noSummoningWanted())
{
// could also use getXXX from object...
server.doSummon(null);
}
else
{
Summoning event = new Summoning(summonInfo.getTarget(),
summonInfo.getDonor(), summonInfo.getUnit());
server.doSummon(event);
}
// Highlight engagements and repaint
gui.actOnDoSummon();
}
public void didSummon(Legion summoner, Legion donor, CreatureType summon)
{
// Create summon event
gui.didSummon(summoner, donor, summon);
}
/*
* Reset the cached reservations.
* Should be called at begin of each recruit turn, if
* reserveRecruit and getReservedCount() are going to be used.
*
*/
public void resetRecruitReservations()
{
recruitReservations.clear();
}
/*
* Reserve one. Expects that getReservedCount() had been called in this
* turn for same creature before called reserveRecruit (= to cache the
* caretakers stack value).
* Returns whether creature can still be recruited (=is available according
* to caretakers stack plus reservations)
*/
public boolean reserveRecruit(CreatureType recruitType)
{
boolean ok = false;
int remain;
Integer count = recruitReservations.get(recruitType);
if (count != null)
{
remain = count.intValue();
recruitReservations.remove(recruitType);
}
else
{
LOGGER.log(Level.WARNING, owningPlayer.getName()
+ " reserveRecruit creature " + recruitType
+ " not fround from hash, should have been created"
+ " during getReservedCount!");
remain = getGame().getCaretaker().getAvailableCount(recruitType);
}
if (remain > 0)
{
remain--;
ok = true;
}
recruitReservations.put(recruitType, Integer.valueOf(remain));
return ok;
}
/*
* On first call (during a turn), cache remaining count from recruiter,
* decrement on each further reserve for this creature.
* This way we are independent of when the changes which are triggered by
* didRecruit influence the caretaker Stack.
* Returns how many creatures can still be recruited (=according
* to caretaker's stack plus reservations)
*/
public int getReservedRemain(CreatureType recruitType)
{
assert recruitType != null : "Can not reserve recruit for null";
int remain;
Integer count = recruitReservations.get(recruitType);
if (count == null)
{
remain = getGame().getCaretaker().getAvailableCount(recruitType);
}
else
{
remain = count.intValue();
recruitReservations.remove(recruitType);
}
// in case someone called getReservedRemain with bypassing the
// reset or reserve methods, to be sure double check against the
// real remaining value.
int realCount = getGame().getCaretaker()
.getAvailableCount(recruitType);
if (realCount < remain)
{
remain = realCount;
}
recruitReservations.put(recruitType, Integer.valueOf(remain));
return remain;
}
/**
* Return a list of Creatures (ignore reservations).
*/
public List<CreatureType> findEligibleRecruits(Legion legion, MasterHex hex)
{
return findEligibleRecruits(legion, hex, false);
}
/**
* Return a list of Creatures and consider reservations if wanted.
*
* @param legion The legion to recruit with.
* @param hex The hex in which to recruit (not necessarily the same as the legion's position). Not null.
* @param considerReservations Flag to determine if reservations should be considered.
* @return A list of possible recruits for the legion in the hex.
*/
public List<CreatureType> findEligibleRecruits(Legion legion,
MasterHex hex, boolean considerReservations)
{
// TODO why not: assert legion != null;
assert hex != null : "Null hex given to find recruits in";
List<CreatureType> recruits = new ArrayList<CreatureType>();
if (legion == null)
{
return recruits;
}
MasterBoardTerrain terrain = hex.getTerrain();
List<CreatureType> tempRecruits = TerrainRecruitLoader
.getPossibleRecruits(terrain, hex);
List<CreatureType> recruiters = TerrainRecruitLoader
.getPossibleRecruiters(terrain, hex);
for (CreatureType creature : tempRecruits)
{
if (getTurnNumber() != 1
|| !options.getOption(Options.noFirstTurnWarlockRecruit)
|| !creature.getName().equals("Warlock"))
{
for (CreatureType lesser : recruiters)
{
if ((TerrainRecruitLoader.numberOfRecruiterNeeded(lesser,
creature, terrain, hex) <= ((LegionClientSide)legion)
.numCreature(lesser))
&& (recruits.indexOf(creature) == -1))
{
recruits.add(creature);
}
}
}
}
// Make sure that the potential recruits are available.
Iterator<CreatureType> it = recruits.iterator();
while (it.hasNext())
{
CreatureType recruit = it.next();
int remaining = getGame().getCaretaker()
.getAvailableCount(recruit);
if (remaining > 0 && considerReservations)
{
remaining = getReservedRemain(recruit);
}
if (remaining < 1)
{
it.remove();
}
}
return recruits;
}
/**
* Return a list of creature name strings.
*
* TODO return List<CreatureType>
*/
public List<String> findEligibleRecruiters(Legion legion,
CreatureType recruit)
{
if (recruit == null)
{
return new ArrayList<String>();
}
Set<CreatureType> recruiters;
MasterHex hex = legion.getCurrentHex();
MasterBoardTerrain terrain = hex.getTerrain();
recruiters = new HashSet<CreatureType>(
TerrainRecruitLoader.getPossibleRecruiters(terrain, hex));
Iterator<CreatureType> it = recruiters.iterator();
while (it.hasNext())
{
CreatureType possibleRecruiter = it.next();
int needed = TerrainRecruitLoader.numberOfRecruiterNeeded(
possibleRecruiter, recruit, terrain, hex);
if (needed < 1 || needed > legion.numCreature(possibleRecruiter))
{
// Zap this possible recruiter.
it.remove();
}
}
List<String> strings = new ArrayList<String>();
for (CreatureType creature : recruiters)
{
strings.add(creature.getName());
}
return strings;
}
/**
* Return a set of hexes with legions that can (still) muster anything
* and are not marked as skip.
*/
public Set<MasterHex> getPossibleRecruitHexes()
{
Set<MasterHex> result = new HashSet<MasterHex>();
for (Legion legion : game.getActivePlayer().getLegions())
{
if (canRecruit(legion) && !legion.getSkipThisTime())
{
result.add(legion.getCurrentHex());
}
}
return result;
}
/** Return a set of hexLabels with legions that could do a recruit
* or undo recruit. Used for "if there is nothing to do in this recruit
* phase, muster phase can immediately be "doneWithRecruit".
*/
private Set<MasterHex> getPossibleRecruitActionHexes()
{
Set<MasterHex> result = new HashSet<MasterHex>();
for (Legion legion : game.getActivePlayer().getLegions())
{
if (canRecruit(legion) || legion.hasRecruited())
{
result.add(legion.getCurrentHex());
}
}
return result;
}
/**
* Check whether any legion has possibility to recruit at all,
* no matter whether it could or has already.
* If there is none, autoDone can automatically be done with recruit
* phase; but if there is something (e.g. autoRecruit has recruited
* something, allow human to override/force him to really confirm "Done".
*
* @return Whether there is any legion that could recruit or undoRecruit
*/
public boolean noRecruitActionPossible()
{
return getPossibleRecruitActionHexes().isEmpty();
}
public MovementClientSide getMovement()
{
return movement;
}
/** Return a set of hexLabels. */
public Set<MasterHex> listTeleportMoves(Legion legion)
{
MasterHex hex = legion.getCurrentHex();
return movement.listTeleportMoves(legion, hex, game.getMovementRoll());
}
/** Return a set of hexLabels. */
public Set<MasterHex> listNormalMoves(Legion legion)
{
return movement.listNormalMoves(legion, legion.getCurrentHex(),
game.getMovementRoll());
}
/**
* Returns status of client's legions
*
* @param legionStatus an array of integers with various status
* states to be set. Array should be initialized to all zeroes
*
* Current array contents:
* [Constants.legionStatusCount] == count of legions
* [Constants.legionStatusMoved] == legions that have moved
* [Constants.legionStatusBlocked] == unmoved legions with no legal move
* [Constants.legionStatusNotVisitedSkippedBlocked] == legions that have not been moved,
* are not blocked and have not been skipped
*/
public void legionsNotMoved(int legionStatus[], boolean have_roll)
{
for (Legion legion : game.getActivePlayer().getLegions())
{
legionStatus[Constants.legionStatusCount]++;
// If don't have roll, can't have moved or been marked skipped
// Can't tell if blocked
if (have_roll == true)
{
if (legion.hasMoved())
{
legionStatus[Constants.legionStatusMoved]++;
}
else
{
Set<MasterHex> teleport = listTeleportMoves(legion);
Set<MasterHex> normal = listNormalMoves(legion);
if (teleport.isEmpty() && normal.isEmpty())
{
legionStatus[Constants.legionStatusBlocked]++;
}
else
{
if (!legion.getVisitedThisPhase()
&& !legion.getSkipThisTime())
{
legionStatus[Constants.legionStatusNotVisitedSkippedBlocked]++;
}
}
}
}
}
}
public Set<MasterHex> findUnmovedLegionHexes(
boolean considerSkippedAsMoved, HashSet<Legion> pendingLegions)
{
Set<MasterHex> result = new HashSet<MasterHex>();
for (Legion legion : game.getActivePlayer().getLegions())
{
if (!legion.hasMoved()
&& !(considerSkippedAsMoved && legion.getSkipThisTime())
&& !pendingLegions.contains(legion))
{
result.add(legion.getCurrentHex());
}
}
return result;
}
/**
* Return a set of hexLabels for the active player's legions with
* 7 or more creatures, and which are not marked as skip this turn.
*/
public Set<MasterHex> findTallLegionHexes()
{
return findTallLegionHexes(7, false);
}
/**
* Return a set of hexLabels for the active player's legions with
* minHeight or more creatures.
*
* @param ignoreSkipFlag Set to true, legion will be considered even if
* it was marked as "skip this time".
*/
public Set<MasterHex> findTallLegionHexes(int minHeight,
boolean ignoreSkipFlag)
{
Set<MasterHex> result = new HashSet<MasterHex>();
for (Legion legion : game.getActivePlayer().getLegions())
{
if (legion.getHeight() >= minHeight
&& (ignoreSkipFlag || !legion.getSkipThisTime()))
{
result.add(legion.getCurrentHex());
}
}
return result;
}
public void notifyServer()
{
if (!isRemote())
{
localServer.setPauseState(false);
/* Calling it locally is "safer". If the stopGame is sent via the
* socket connection, the call of disposeClientOriginated below
* might be handled so fast, that it's connection closed exception
* on server side causes the server to handle a withdrawal for it.
* And if there is only two players AND autoQuit is set, this might
* terminate the application despite the fact that one wanted to do
* e.g. New Game or Load Game.
*/
localServer.initiateQuitGame();
}
else
{
// If remote clients do New Game etc, this does not directly cause
// the server to do anything as above, so: stopGame commented out.
// If after this remote client gone there is only one player left,
// i.e. game is then over, server side will act accordingly.
//server.stopGame();
}
disposeClientOriginated();
}
public boolean isMyLegion(Legion legion)
{
return !spectator && owningPlayer.equals(legion.getPlayer());
}
public boolean isMyTurn()
{
return !spectator && owningPlayer.equals(getActivePlayer());
}
public boolean isFightPhase()
{
if (game != null)
{
return game.isPhase(Phase.FIGHT);
}
return false;
}
public boolean isMyBattlePhase()
{
// check also for phase, because delayed callbacks could come
// after our phase is over but activePlayerName not updated yet
return isAlive() && owningPlayer.equals(getBattleActivePlayer())
&& game.isPhase(Phase.FIGHT);
}
public void pingRequest(long requestTime)
{
// Dummy, SocketClientThread handles this already.
}
public void logMsgToServer(String severity, String message)
{
server.logMsgToServer(severity, message);
}
public boolean testBattleMove(BattleCritter battleUnit, BattleHex hex)
{
if (showBattleMoves(battleUnit).contains(hex))
{
battleUnit.setCurrentHex(hex);
return true;
}
return false;
}
/** Wait for aiDelay. */
private void aiPause()
{
// TODO why is this not set up once, when Client is created?
if (delay < 0)
{
setupDelay();
}
try
{
Thread.sleep(delay);
}
catch (InterruptedException ex)
{
LOGGER.log(Level.SEVERE, "Client.aiPause() interrupted", ex);
}
}
private void setupDelay()
{
delay = options.getIntOption(Options.aiDelay);
// If not in autoPlay mode, set it to minimum, because then it is a
// human player who just uses some autoXXX functionality,
// and we don't want a human to have to wait after certain activities
// the AI does for him.
if (!isAutoplayActive() || delay < Constants.MIN_AI_DELAY)
{
delay = Constants.MIN_AI_DELAY;
}
else if (delay > Constants.MAX_AI_DELAY)
{
delay = Constants.MAX_AI_DELAY;
}
}
public Game getGame()
{
return game;
}
public GameClientSide getGameClientSide()
{
return game;
}
public Options getOptions()
{
return options;
}
public String getClientName()
{
if (getOwningPlayer() != null)
{
return getOwningPlayer().getName();
}
else
{
return "<ownplayernotset>";
}
}
/** TODO get from Variant instead of static TerrainRecruitLoader access
* Just forwarding the query, to get at least the GUI classes get rid of
* dependency to static TerrainRecruitLoader access.
*
* {@link TerrainRecruitLoader#getPossibleRecruits(MasterBoardTerrain, MasterHex)}
*/
public List<CreatureType> getPossibleRecruits(MasterBoardTerrain terrain,
MasterHex hex)
{
return TerrainRecruitLoader.getPossibleRecruits(terrain, hex);
}
/** TODO get from Variant instead of static TerrainRecruitLoader access
* Just forwarding the query, to get at least the GUI classes get rid of
* dependency to static TerrainRecruitLoader access.
*
* {@link TerrainRecruitLoader#numberOfRecruiterNeeded(CreatureType,
CreatureType, MasterBoardTerrain, MasterHex)}
*/
public int numberOfRecruiterNeeded(CreatureType recruiter,
CreatureType recruit, MasterBoardTerrain terrain, MasterHex hex)
{
return TerrainRecruitLoader.numberOfRecruiterNeeded(recruiter,
recruit, terrain, hex);
}
/**
* Return a collection of all possible terrains.
*
* @return A collection containing all instances of {@link MasterBoardTerrain}.
*/
public Collection<MasterBoardTerrain> getTerrains()
{
return game.getVariant().getTerrains();
}
public static class ConnectionInitException extends Exception
{
public ConnectionInitException(String reason)
{
super(reason);
}
}
public void setPreferencesCheckBoxValue(String name, boolean value)
{
gui.setPreferencesCheckBoxValue(name, value);
}
public void setPreferencesRadioButtonValue(String name, boolean value)
{
gui.setPreferencesRadioButtonValue(name, value);
}
public void editAddCreature(String markerId, String creatureType)
{
localServer.getGame().editModeAddCreature(markerId, creatureType);
}
public void editRemoveCreature(String markerId, String creatureType)
{
localServer.getGame().editModeRemoveCreature(markerId, creatureType);
}
public void editRelocateLegion(String markerId, String hexLabel)
{
localServer.getGame().editModeRelocateLegion(markerId, hexLabel);
}
public void destroyLegion(Legion legion)
{
server.cheatModeDestroyLegion(legion);
}
}