package onlinefrontlines.game;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Random;
import java.util.Calendar;
import java.util.ArrayList;
import java.awt.Point;
import java.sql.SQLException;
import javax.mail.internet.InternetAddress;
import onlinefrontlines.Constants;
import onlinefrontlines.auth.*;
import onlinefrontlines.game.actions.*;
import onlinefrontlines.taglib.CacheTag;
import onlinefrontlines.userstats.*;
import onlinefrontlines.utils.GlobalProperties;
import onlinefrontlines.utils.IllegalRequestException;
import onlinefrontlines.utils.Mailer;
import onlinefrontlines.utils.Tools;
/**
* Current state for an active game
*
* @author jorrit
*
* Copyright (C) 2009-2013 Jorrit Rouwe
*
* This file is part of Online Frontlines.
*
* Online Frontlines is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Online Frontlines is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Online Frontlines. If not, see <http://www.gnu.org/licenses/>.
*/
public final class GameState
{
/********* BEGIN SYNCHRONIZED BLOCK BETWEEN FLASH AND JAVA *********/
/**
* Id of the game
*/
public int id = -1;
/**
* The players that are playing this game
*/
private final Player[] players = new Player[2];
/**
* Indicate which players are ready
*/
public boolean playerReady[] = { false, false };
/**
* If faction 1 starts or 2
*/
public final boolean faction1Starts;
/**
* Faction of current player
*/
public Faction currentPlayer;
/**
* Number of turns played
*/
public int turnNumber = 0;
/**
* Time when the current turn ends
*/
public long turnEndTime;
/**
* Initial terrain owners, to calculate score
*/
public Faction[] initialTerrainOwners;
/**
* Score objects
*/
public final Score[] scores = { new Score(), new Score() };
/**
* Players that requested a draw
*/
public boolean drawRequested[] = { false, false };
/**
* If the game has ended this will indicate the winning faction
*/
public Faction winningFaction = Faction.invalid;
/**
* All units
*/
private final ArrayList<UnitState> units = new ArrayList<UnitState>();
/**
* Quick access to a unit by its location
*/
private final UnitState[][] unitGrid;
/**
* Quick access to a unit
*/
private final HashMap<Integer, UnitState> unitMap = new HashMap<Integer, UnitState>();
/**
* Get player
*/
public Player getPlayer(Faction faction)
{
switch (faction)
{
case f1:
return players[0];
case f2:
return players[1];
default:
return null;
}
}
/**
* Get player user id
*/
public int getPlayerId(Faction faction)
{
Player player = getPlayer(faction);
return player != null? player.id : -1;
}
/**
* Get score object
*/
public Score getScore(Faction faction)
{
switch (faction)
{
case f1:
return scores[0];
case f2:
return scores[1];
default:
return null;
}
}
/**
* Join game for second time
*/
public void reJoinGame(Faction faction, User user)
{
players[faction == Faction.f1? 0 : 1] = new Player(user);
}
/**
* Register unit with this game
*/
public void registerUnit(UnitState unit)
{
assert(unitMap.get(unit.id) == null);
unitMap.put(unit.id, unit);
}
/**
* Unregister unit
*/
public void unregisterUnit(UnitState unit)
{
assert(unitMap.get(unit.id) == unit);
unitMap.remove(unit.id);
}
/**
* Find a unit by its id
*/
public UnitState getUnitById(int id)
{
return unitMap.get(id);
}
/**
* Add unit at particular location
*/
public void addUnit(UnitState unit)
{
// Add to grid and array
assert(unitGrid[unit.locationX][unit.locationY] == null);
unitGrid[unit.locationX][unit.locationY] = unit;
assert(!units.contains(unit));
units.add(unit);
updateIdentification();
}
/**
* Remove a unit
*/
public void removeUnit(UnitState unit)
{
// Remove from grid and array
assert(unitGrid[unit.locationX][unit.locationY] == unit);
unitGrid[unit.locationX][unit.locationY] = null;
assert(units.contains(unit));
units.remove(unit);
}
/**
* Get unit at particular location
*/
public UnitState getUnit(int x, int y)
{
return unitGrid[x][y];
}
/**
* Get all units
*/
public ArrayList<UnitState> getUnits()
{
return units;
}
/**
* Get terrain at (x, y)
*/
public TerrainConfig getTerrainAt(int x, int y)
{
return mapConfig.getTerrainAt(x, y);
}
/**
* Get terrain unit is on
*/
public TerrainConfig getTerrain(UnitState unit)
{
return unit.container == null? getTerrainAt(unit.locationX, unit.locationY) : getTerrain(unit.container);
}
/**
* Returns terrain owner at x, y
*/
public Faction getTerrainOwnerAt(int x, int y)
{
return tileOwners[x + y * mapConfig.sizeX];
}
/**
* Sets terrain owner at x, y
*/
public void setTerrainOwnerAt(int x, int y, Faction newOwner)
{
Faction initialOwner = getInitialTerrainOwnerAt(x, y);
Faction oldOwner = getTerrainOwnerAt(x, y);
tileOwners[x + y * mapConfig.sizeX] = newOwner;
if (newOwner != oldOwner)
{
int victoryPoints = getTerrainAt(x, y).victoryPoints;
if (victoryPoints > 0)
{
// Award points if tile captured
if (newOwner != initialOwner)
{
Score score = getScore(newOwner);
if (score != null)
{
score.numberOfTilesOwned++;
score.victoryPointsForTiles += victoryPoints;
}
}
// Deduct points if tile is lost again
if (oldOwner != initialOwner)
{
Score score = getScore(oldOwner);
if (score != null)
{
score.numberOfTilesOwned--;
score.victoryPointsForTiles -= victoryPoints;
}
}
}
}
}
/**
* Update ownership of tile unit is on
*/
public void updateTerrainOwner(UnitState unit)
{
if (unit.armour > 0
&& unit.unitConfig.unitClass == UnitClass.land
&& getTerrain(unit).victoryPoints > 0)
setTerrainOwnerAt(unit.locationX, unit.locationY, unit.faction);
}
/**
* Calculate initial terrain state
*/
public void calculateInitialTerrainOwners()
{
initialTerrainOwners = new Faction[mapConfig.sizeX * mapConfig.sizeY];
// Loop through tiles
for (int x = 0; x < mapConfig.sizeX; ++x)
for (int y = 0; y < mapConfig.sizeY; ++y)
{
// Reset owner if this type of terrain doesn't give any points
TerrainConfig terrain = getTerrainAt(x, y);
if (terrain.victoryPoints <= 0)
setTerrainOwnerAt(x, y, Faction.none);
// Store owner
initialTerrainOwners[x + y * mapConfig.sizeX] = getTerrainOwnerAt(x, y);
}
}
/**
* Returns terrain owner at x, y when the game started
*/
public Faction getInitialTerrainOwnerAt(int x, int y)
{
return initialTerrainOwners[x + y * mapConfig.sizeX];
}
/**
* Get terrain unit is on (used for attacking, with special case for air)
*/
public TerrainConfig getAttackTerrain(UnitState unit)
{
// Flying units are always in the air
if (unit.unitConfig.unitClass == UnitClass.air)
return TerrainConfig.airTerrain;
// Otherwise look for the tile we're on
return getTerrain(unit);
}
/**
* Get terrain unit is on (used for attacking, with special case for air)
*/
public TerrainConfig getAttackTerrain(UnitState unit, int x, int y)
{
// Flying units are always in the air
if (unit.unitConfig.unitClass == UnitClass.air)
return TerrainConfig.airTerrain;
return getTerrainAt(x, y);
}
/**
* Check if a unit can be set up on on tile x, y
*/
public boolean canUnitBeTeleportedTo(UnitState unitState, int x, int y)
{
// Check if base is too close to another base
if (unitState.unitConfig.isBase)
for (UnitState u : units)
if (u != unitState
&& u.faction == unitState.faction
&& u.unitConfig.isBase
&& u.getDistanceTo(x, y) <= 2)
return false;
return canUnitBeSetupOnHelper(mapConfig, unitState.initialUnitConfig, unitState.faction, x, y);
}
/**
* Check if a unit can be set up on tile x, y
*/
public static boolean canUnitBeSetupOnHelper(MapConfig mapConfig, UnitConfig unitConfig, Faction faction, int x, int y)
{
// Unit must be in playable area and the terrain must be owned by the correct faction
if (!mapConfig.isTileInPlayableArea(x, y)
|| mapConfig.getTerrainOwnerAt(x, y) != faction)
return false;
// If unit needs to be set up next to a specific terrain, validate it
if (unitConfig.unitSetupNextTo.size() > 0)
{
boolean found = false;
for (Point n : mapConfig.getNeighbours(x, y))
if (unitConfig.unitSetupNextTo.contains(mapConfig.getTerrainAt(n.x, n.y).id))
{
found = true;
break;
}
if (!found)
return false;
}
// If unit can move here
TerrainConfig terrain = mapConfig.getTerrainAt(x, y);
if (unitConfig.canMoveOn(terrain))
return true;
// If terrain is in the list where the unit can be set up on
if (unitConfig.unitSetupOn.contains(terrain.id))
return true;
return false;
}
/**
* Move unit to new location
*/
public void moveUnit(UnitState unit, int x, int y)
{
unitGrid[unit.locationX][unit.locationY] = null;
unit.locationX = x;
unit.locationY = y;
unitGrid[x][y] = unit;
updateIdentification();
}
/**
* Move unit into container unit
*/
public void moveUnitInContainer(UnitState unit, UnitState container)
{
// Remove from grid
removeUnit(unit);
// Add to container
container.addUnit(unit);
}
/**
* Remove unit from container
*/
public void moveUnitOutOfContainer(UnitState unit, UnitState container, int x, int y)
{
// Remove from container
container.removeUnit(unit);
// Place on new position
unit.locationX = x;
unit.locationY = y;
// Add to grid
addUnit(unit);
// Check if unit can be seen again
updateIdentification();
}
/**
* Get movement cost when unit moves (1 tile) to new location
*/
public int getMovementCost(UnitState unit, int x, int y)
{
// Get terrain
TerrainConfig terrain = getTerrainAt(x, y);
// Modify terrain under a base to type that unit supports
UnitState base = getUnit(x, y);
if (base != null && base.canHold(unit))
{
if (unit.unitConfig.unitClass == UnitClass.water)
terrain = TerrainConfig.waterTerrain;
else
terrain = TerrainConfig.plainsTerrain;
}
// Get cost for this move
return unit.unitConfig.getMovementCost(terrain);
}
/**
* Set the winning faction and end the game
*/
public void setWinningFaction(Faction faction)
{
// Mark game as ended
winningFaction = faction;
// Identify all units
for (UnitState u : units)
u.identifyUnitRecursive();
try
{
// Send new scores to client
if (players[0] != null)
execute(new ActionPlayerProperties(Faction.f1), false);
if (players[1] != null)
execute(new ActionPlayerProperties(Faction.f2), false);
}
catch (IllegalRequestException e)
{
Tools.logException(e);
}
}
/**
* If user can request draw or surrender (to prevent cheating)
*/
public boolean userCanTerminateGame()
{
return scores[0].getTotalScore() > 100 || scores[1].getTotalScore() > 100;
}
/**
* Dump state of the game for comparison
*
* @param localFaction The faction that the state should be simulated for
*/
public String dumpState(Faction localFaction)
{
// Dump general properties
StringBuilder rv = new StringBuilder(10240);
rv.append("turnNumber = ");
rv.append(turnNumber);
rv.append("\ncurrentPlayer = ");
rv.append(Faction.toInt(currentPlayer));
// Dump initial terrain ownership
rv.append("\ninitialTerrainOwner =\n");
for (int y = 0; y < mapConfig.sizeY; ++y)
{
rv.append(Faction.toInt(getInitialTerrainOwnerAt(0, y)));
for (int x = 1; x < mapConfig.sizeX; ++x)
{
rv.append(", ");
rv.append(Faction.toInt(getInitialTerrainOwnerAt(x, y)));
}
rv.append("\n");
}
// Dump terrain ownership
rv.append("terrainOwner =\n");
for (int y = 0; y < mapConfig.sizeY; ++y)
{
rv.append(Faction.toInt(getTerrainOwnerAt(0, y)));
for (int x = 1; x < mapConfig.sizeX; ++x)
{
rv.append(", ");
rv.append(Faction.toInt(getTerrainOwnerAt(x, y)));
}
rv.append("\n");
}
// Dump scores
for (int i = 0; i < 2; ++i)
{
rv.append("scores[");
rv.append(i);
rv.append("] = ");
scores[i].dumpState(rv);
rv.append("\n");
}
// Dump units
rv.append("units =\n");
for (int y = 0; y < mapConfig.sizeY; ++y)
for (int x = 0; x < mapConfig.sizeX; ++x)
{
UnitState unit = getUnit(x, y);
if (unit != null)
unit.dumpState(rv, localFaction, 0);
}
return rv.toString();
}
/********* END SYNCHRONIZED BLOCK BETWEEN FLASH AND JAVA *********/
/**
* Settings for the game
*/
public final CountryConfig countryConfig;
/**
* Cached map config
*/
public final MapConfig mapConfig;
/**
* Which color faction 1 is
*/
public final boolean faction1IsRed;
/**
* Lobby that hosted the game
*/
public final int lobbyId;
/**
* Country location
*/
public final int attackedCountryX;
public final int attackedCountryY;
public final int defendedCountryX;
public final int defendedCountryY;
/**
* If we want to send an email at the end of each turn
*/
public final boolean playByMail;
/**
* Last communication time with client
*/
private long lastCommunicationTime[] = { 0, 0 };
/**
* Flag to indicate if the current player did something this turn
*/
public boolean currentPlayerIdle = true;
/**
* Owning faction of the tiles
*/
private final Faction[] tileOwners;
/**
* True if state mismatch was already logged for this game
*/
public boolean loggedStateMismatch = false;
/**
* Helper class that determines which faction should receive which actions
*/
public static class FilteredList
{
public ArrayList<String> sendList = new ArrayList<String>();
public ArrayList<Action> pendingList = new ArrayList<Action>();
}
/**
* Actions filtered per receiving faction
*/
private final FilteredList[] filteredList = { new FilteredList(), new FilteredList(), new FilteredList() };
/**
* All actions performed, linefeed separated
*/
public String actions = "";
/**
* If the state has changed and the game state needs to be written to the database
*/
private boolean needsToBeWrittenToDb = false;
/**
* Last time an action was received
*/
public long lastActionTime;
/**
* Orred mask of all victory categories that are still active in the game
*/
private int initialVictoryCategories[] = { 0, 0 };
/**
* Scores from previous round
*/
public final Score[] previousTurnScores = { new Score(), new Score() };
/**
* Stats for all units from a faction
*/
public static class AllUnitStats
{
public HashMap<Integer, UnitStats> unitToStatsMap = new HashMap<Integer, UnitStats>();
/**
* Get unit stats for particular unit, if it doesn't exist yet a new entry will be created
*/
public UnitStats getOrCreate(int unitId)
{
// Find existing
UnitStats u = unitToStatsMap.get(unitId);
if (u != null)
return u;
// Create new
u = new UnitStats();
u.unitId = unitId;
unitToStatsMap.put(unitId, u);
return u;
}
}
/**
* Keeps track of unit stats per player
*/
private AllUnitStats[] unitStats = { new AllUnitStats(), new AllUnitStats() };
/**
* Random number generator
*/
public Random random;
/**
* Constructor
*
* @param countryConfig The configuration to start this game with
* @param faction1IsRed If faction 1 is red
* @param faction1Starts If faction 1 moves first
* @param lobbyId Lobby where this game belongs to (or -1)
* @param attackedCountryX X location of country that is being attacked from (or -1)
* @param attackedCountryY Y location of country that is being attacked from (or -1)
* @param defendedCountryX X location of country that is being defended from (or -1)
* @param defendedCountryY Y location of country that is being defended from (or -1)
* @param playByMail If this is a play by mail game
* @param randomSeed Seed to use for the random number generator (or -1 if a random seed should be chosen)
*/
public GameState(CountryConfig countryConfig, boolean faction1IsRed, boolean faction1Starts, int lobbyId, int attackedCountryX, int attackedCountryY, int defendedCountryX, int defendedCountryY, boolean playByMail, long randomSeed)
{
// Store config
this.countryConfig = countryConfig;
this.mapConfig = countryConfig.getMapConfig();
this.faction1IsRed = faction1IsRed;
this.faction1Starts = faction1Starts;
this.lobbyId = lobbyId;
this.attackedCountryX = attackedCountryX;
this.attackedCountryY = attackedCountryY;
this.defendedCountryX = defendedCountryX;
this.defendedCountryY = defendedCountryY;
this.playByMail = playByMail;
// Create random number generator
random = randomSeed != -1? new Random(randomSeed) : new Random();
// Starting the game will first toggle the faction so currentPlayer starts at the other faction
currentPlayer = faction1Starts? Faction.f2 : Faction.f1;
// Create unit grid
this.unitGrid = new UnitState[mapConfig.sizeX][mapConfig.sizeY];
// Clone owners (will be modified when units start moving around)
tileOwners = mapConfig.cloneTileOwners();
// Mark now as last action received time
lastActionTime = Calendar.getInstance().getTime().getTime();
}
/**
* Join game first time
*/
public void joinGame(Faction faction, User user) throws SQLException, IllegalRequestException
{
// Join the game
reJoinGame(faction, user);
// Execute action
execute(new ActionPlayerJoin(faction, user.id), true);
// Add to database
GameStateDAO.joinGame(id, user.id, faction);
// Make sure the text appears in the top bar
clearPageTopCache();
// Check if both players are in now
if (players[0] != null && players[1] != null)
{
// Start timer
resetTimeLeft();
// Both players can start deploying
sendMail(Faction.f1, "mail/PBMDeployUnits.jsp", "Deploy your units");
sendMail(Faction.f2, "mail/PBMDeployUnits.jsp", "Deploy your units");
}
}
/**
* Get faction of a particular user
* @param user User to get faction for
*/
public Faction getUserFaction(User user)
{
if (user != null && players[0] != null && players[0].id == user.id)
return Faction.f1;
else if (user != null && players[1] != null && players[1].id == user.id)
return Faction.f2;
else
return Faction.none;
}
/**
* Check a faction can make a move
* @faction Faction to check
*/
public boolean canPerformAction(Faction faction)
{
if (faction == Faction.none)
{
return false;
}
else if (turnNumber == 0)
{
return !playerReady[faction == Faction.f1? 0 : 1]
&& winningFaction == Faction.invalid;
}
else
{
return currentPlayer == faction
&& winningFaction == Faction.invalid;
}
}
/**
* Class to hold previous state
*/
private static class OldState
{
public int turnNumber;
public Faction winningFaction;
public Faction currentPlayer;
public boolean drawRequested[];
public OldState(GameState state)
{
turnNumber = state.turnNumber;
winningFaction = state.winningFaction;
currentPlayer = state.currentPlayer;
drawRequested = state.drawRequested.clone();
}
}
/**
* Execute an action
*/
public void execute(Action action, boolean addToDb) throws IllegalRequestException
{
// Remember old state
OldState oldState = new OldState(this);
// Update pending actions
for (FilteredList l : filteredList)
for (Action a : l.pendingList)
a.pendingActionUpdate();
// Set game state
action.setGameState(this);
// Perform the action
action.doAction(addToDb);
// Add to database
if (addToDb)
{
actions += action.toString() + "\n";
needsToBeWrittenToDb = true;
}
// Make sure action gets its first update
action.pendingActionUpdate();
// Add to filtered list
addActionToFilteredList(action, Faction.f1);
addActionToFilteredList(action, Faction.f2);
addActionToFilteredList(action, Faction.none);
// Get units that are no longer detected
for (UnitState u : unitMap.values())
if (u.checkDetectionLost())
{
u.confirmDetectionLost();
execute(new ActionRemoveUnit(u.id), false);
execute(new ActionCreateUnit(u.id, u.locationX, u.locationY, u.container != null? u.container.id : -1, u.faction, u.unitConfig.id, true), false);
}
// Send properties for players if they just joined
if (action instanceof ActionPlayerJoin)
execute(new ActionPlayerProperties(((ActionPlayerJoin)action).getFaction()), false);
// Add identification if a create unit was processed
if (action instanceof ActionCreateUnit)
execute(new ActionIdentifyUnit(((ActionCreateUnit)action).getUnitId()), false);
// Optionally execute some other actions
if (addToDb)
postExecuteAction(oldState);
}
/**
* Get victory categories still available in the game (victory category 0 sets bit 0 etc.)
*
* @param f Faction to test for
* @return Mask with bits set for which categories still exist
*/
public int getVictoryCategories(Faction f)
{
int categories = 0;
for (UnitState u : units)
if (u.faction == f)
categories |= getVictoryCategoriesRecursive(u);
return categories;
}
/**
* Recursive helper function
*/
public int getVictoryCategoriesRecursive(UnitState unit)
{
int categories = 1 << unit.unitConfig.victoryCategory;
for (UnitState c : unit.containedUnits)
categories |= getVictoryCategoriesRecursive(c);
return categories;
}
/**
* Determine if the game should start or end
*/
private void postExecuteAction(OldState oldState) throws IllegalRequestException
{
// Your turn messages
if (oldState.currentPlayer != currentPlayer)
sendMail(currentPlayer, "mail/PBMYourTurn.jsp", "Your turn");
// Draw request messages
if (drawRequested[0] && !drawRequested[1] && drawRequested[0] != oldState.drawRequested[0])
sendMail(Faction.f2, "mail/PBMDrawRequested.jsp", "Draw requested");
if (drawRequested[1] && !drawRequested[0] && drawRequested[1] != oldState.drawRequested[1])
sendMail(Faction.f1, "mail/PBMDrawRequested.jsp", "Draw requested");
// Check if turn number / current player changed
if (oldState.turnNumber != turnNumber
|| oldState.currentPlayer != currentPlayer)
{
// Mark in database
needsToBeWrittenToDb = true;
}
// Check if this action triggered end game
if (oldState.winningFaction != winningFaction)
{
// Mark in database
needsToBeWrittenToDb = true;
// Only valid games that are on a published map get their stats added
if (players[0] != null
&& players[1] != null
&& players[0].id != Constants.USER_ID_AI
&& players[1].id != Constants.USER_ID_AI
&& countryConfig.publishState == PublishState.published)
{
// Update stats
for (int p = 0; p < 2; ++p)
{
int userId = players[p].id;
// Determine if this player won / lost / drew
Faction pf = p == 0? Faction.f1 : Faction.f2;
boolean won = winningFaction == pf;
boolean lost = winningFaction == Faction.opposite(pf);
try
{
// Set user stats for this game
UserStats s = new UserStats(userId);
s.gamesPlayed = 1;
s.gamesWon = won? 1 : 0;
s.gamesLost = lost? 1 : 0;
s.totalPoints = (int)((won? 2.0 : (lost? 0.5 : 1.0)) * scores[p].getTotalScore());
// Accumulate totals
UserStatsDAO.accumulateStats(s);
}
catch (SQLException e)
{
Tools.logException(e);
}
try
{
// Accumulate unit stats
for (UnitStats u : unitStats[p].unitToStatsMap.values())
UnitStatsDAO.accumulateUnitStats(userId, u);
}
catch (SQLException e)
{
Tools.logException(e);
}
}
}
// End game messages
sendGameResultMail(Faction.f1);
sendGameResultMail(Faction.f2);
}
// Determine game start
if (turnNumber == 0 && playerReady[0] && playerReady[1])
execute(new ActionEndTurn(), true);
// Determine winner
if (turnNumber > 0 && winningFaction == Faction.invalid)
{
// Check score
if (scores[0].getTotalScore() >= countryConfig.scoreLimit)
execute(new ActionEndGame(Faction.f1), true);
else if (scores[1].getTotalScore() >= countryConfig.scoreLimit)
execute(new ActionEndGame(Faction.f2), true);
// Check if all units in a victory category were destroyed
else if (initialVictoryCategories[0] != (getVictoryCategories(Faction.f1) & initialVictoryCategories[0]))
execute(new ActionEndGame(Faction.f2), true);
else if (initialVictoryCategories[1] != (getVictoryCategories(Faction.f2) & initialVictoryCategories[1]))
execute(new ActionEndGame(Faction.f1), true);
// Check pending draw requests
else if (drawRequested[0] && drawRequested[1])
execute(new ActionEndGame(Faction.none), true);
}
// Mark now as last action received time
lastActionTime = Calendar.getInstance().getTime().getTime();
}
/**
* Get an Action list filtered for a specific faction
*
* @param faction Receiving faction
*/
public FilteredList getFilteredList(Faction faction)
{
switch (faction)
{
case f1:
return filteredList[0];
case f2:
return filteredList[1];
default:
return filteredList[2];
}
}
/**
* Comparator for sorting units
*/
private static class SortOnPendingActionSortKey implements Comparator<Action>
{
public int compare(Action a1, Action a2)
{
return a1.pendingActionGetSortKey() - a2.pendingActionGetSortKey();
}
}
/**
* Add action to filtered list
*/
private void addActionToFilteredList(Action action, Faction faction)
{
FilteredList list = getFilteredList(faction);
switch (action.pendingActionGetReceiveTime(faction))
{
case now:
// Check if previous actions need to be sent now
processPendingList(faction);
// Action accepted
list.sendList.add(action.toString(faction));
break;
case later:
// Action cannot yet be sent to remote
list.pendingList.add(action);
break;
case never:
// Do not add to list
break;
}
}
/**
* Search through the pending list for actions that need to be sent now
*/
private void processPendingList(Faction faction)
{
FilteredList list = getFilteredList(faction);
// Search through pending list
ArrayList<Action> executeNowList = new ArrayList<Action>();
ArrayList<Action> executeNeverList = new ArrayList<Action>();
for (Action a : list.pendingList)
switch (a.pendingActionGetReceiveTime(faction))
{
case now:
executeNowList.add(a);
break;
case never:
executeNeverList.add(a);
break;
}
// Remove actions in the remove list
for (Action a : executeNeverList)
list.pendingList.remove(a);
// Sort pending actions so that actions that depend on other actions
// are received last (units that are contained in a container)
Action[] sortedList = executeNowList.toArray(new Action[0]);
Arrays.sort(sortedList, new SortOnPendingActionSortKey());
for (Action a : sortedList)
{
list.pendingList.remove(a);
list.sendList.add(a.toString(faction));
}
}
/**
* Initialize the game
*/
public void initGame() throws IllegalRequestException, DeploymentFailedException
{
// Create initial random deployment
DeploymentHelper helper = new DeploymentHelper(random);
ArrayList<UnitState> deployment = helper.getDeployment(countryConfig);
for (UnitState u : deployment)
deployUnit(u);
// Initial time out
resetTimeLeft();
}
/**
* Deploy a unit and its children
*/
private void deployUnit(UnitState u) throws IllegalRequestException
{
// Add unit
execute(new ActionCreateUnit(u.id, u.locationX, u.locationY, u.container != null? u.container.id : -1, u.faction, u.unitConfig.id, false), true);
// Add children
for (UnitState c : u.containedUnits)
deployUnit(c);
}
/**
* Determine initial victory categories that are present in the game
*/
public void determineInitialVictoryCategories()
{
initialVictoryCategories[0] = getVictoryCategories(Faction.f1);
initialVictoryCategories[1] = getVictoryCategories(Faction.f2);
}
/**
* Unit has changed, check if identification state needs to be updated
*/
public void updateIdentification()
{
if (turnNumber > 0)
for (UnitState unit : units)
{
boolean detected = false;
boolean identified = unit.identifiedByEnemy;
for (UnitState otherUnit : units)
if (otherUnit.faction != unit.faction)
{
int distance = MapConfig.getDistance(unit.locationX, unit.locationY, otherUnit.locationX, otherUnit.locationY);
if (distance <= unit.unitConfig.beDetectedRange)
{
detected = true;
if (!countryConfig.fogOfWarEnabled)
{
identified = true;
break;
}
}
if (distance <= otherUnit.unitConfig.visionRange)
{
detected = true;
identified = true;
break;
}
}
// Update detected state of unit
if (!detected)
unit.setDetectedRecursive(false, Integer.MAX_VALUE);
else
unit.setDetected(true);
// Update identified state of unit
if (identified)
{
unit.setDetectedRecursive(true, 2);
unit.identifiedByEnemy = true;
}
else
unit.identifiedByEnemy = false;
}
}
/**
* Reset time left
*/
public void resetTimeLeft()
{
// Get time of this turn
long timeThisTurn = 0;
if (players[0] == null || players[1] == null)
{
// Game cannot last longer than an hour without two players
timeThisTurn = 60L * 60L * 1000L;
}
else if (lobbyId != 0)
{
// Playing from a lobby
Player player = getPlayer(currentPlayer);
if (player != null && UserRank.getLevel(player.id) > Constants.HIGH_RANKED_USER_LEVEL)
timeThisTurn = Constants.PLAY_LOBBY_TURN_TIME_HIGH_RANKED_USER;
else
timeThisTurn = Constants.PLAY_LOBBY_TURN_TIME;
}
else if (playByMail)
{
// Play by mail
timeThisTurn = Constants.PLAY_BY_MAIL_TURN_TIME;
}
else
{
// Play live
int numUnits = 0;
for (UnitState u : getUnits())
if (u.faction == currentPlayer)
++numUnits;
if (turnNumber == 0)
timeThisTurn = Constants.GAME_SETUP_TIME;
else
timeThisTurn = Constants.GAME_MIN_TIME_PER_TURN + numUnits * Constants.GAME_TIME_EXTRA_PER_UNIT;
}
// Set time
turnEndTime = Calendar.getInstance().getTime().getTime() + timeThisTurn;
needsToBeWrittenToDb = true;
}
/**
* Get time left in turn
*
* @return Time left in seconds or -1 for infinite
*/
public long getTimeLeft()
{
if (winningFaction != Faction.invalid)
return -1;
return Math.max(turnEndTime - Calendar.getInstance().getTime().getTime(), 0);
}
/**
* Check if turn has timed out and do according action
*
* @throws IllegalRequestException
*/
public void checkTimeout() throws IllegalRequestException
{
if (getTimeLeft() == 0)
{
// Flag timeout
execute(new ActionTimeOut(), true);
if (turnNumber == 0)
{
if (getPlayer(Faction.f2) != null)
{
// Start the game
execute(new ActionEndTurn(), true);
}
else
{
// Draw
execute(new ActionEndGame(Faction.none), true);
}
}
else if (currentPlayerIdle)
{
// Opposite faction is winner
Faction winner = Faction.opposite(currentPlayer);
execute(new ActionEndGame(winner), true);
}
else
{
// End turn for current player
execute(new ActionEndTurn(), true);
}
}
}
/**
* Mark that a player connected
*/
public void markPlayerConnected(Faction faction)
{
// Remember last communication time for player
switch (faction)
{
case f1:
lastCommunicationTime[0] = Calendar.getInstance().getTime().getTime();
break;
case f2:
lastCommunicationTime[1] = Calendar.getInstance().getTime().getTime();
break;
}
}
/**
* Check if a specific player recently communicated with the cerver
*/
public boolean isPlayerConnected(Faction faction)
{
// AI is always connected
Player player = getPlayer(faction);
if (player != null && player.id == Constants.USER_ID_AI)
return true;
// Otherwise check last communication
switch (faction)
{
case f1:
return Calendar.getInstance().getTime().getTime() - lastCommunicationTime[0] < Constants.CLIENT_POLL_TIMEOUT;
case f2:
return Calendar.getInstance().getTime().getTime() - lastCommunicationTime[1] < Constants.CLIENT_POLL_TIMEOUT;
default:
return false;
}
}
/**
* Send a mail indicating game results
*/
private void sendGameResultMail(Faction recipientFaction)
{
if (!playByMail)
return;
if (recipientFaction == winningFaction)
sendMail(recipientFaction, "mail/PBMGameWon.jsp", "You won");
else if (recipientFaction == Faction.opposite(winningFaction))
sendMail(recipientFaction, "mail/PBMGameLost.jsp", "You lost");
else
sendMail(recipientFaction, "mail/PBMGameDraw.jsp", "Game is a draw");
}
/**
* Send e-mail to player player
*/
public void sendMail(Faction recipientFaction, String templateJsp, String title)
{
// Check if this is a play by mail game
if (!playByMail)
return;
try
{
// Determine sender and recicpient
Faction senderFaction = Faction.opposite(recipientFaction);
User sender = getPlayer(senderFaction).getUser();
User recipient = getPlayer(recipientFaction).getUser();
// Check if recipient wants email
if (!recipient.getHasEmail() || !recipient.receiveGameEventsByMail)
return;
// Determine parameters
Mailer.Mail mail = new Mailer.Mail();
mail.params.put("p1Name", getPlayer(Faction.f1).getUser().username);
mail.params.put("p2Name", getPlayer(Faction.f2).getUser().username);
mail.params.put("p1Score", Integer.toString(scores[0].getTotalScore()));
mail.params.put("p2Score", Integer.toString(scores[1].getTotalScore()));
mail.params.put("countryName", countryConfig.name);
mail.params.put("targetScore", Integer.toString(countryConfig.scoreLimit));
int s = Faction.toInt(senderFaction) - 1;
mail.params.put("unitsDestroyed", Integer.toString(scores[s].numberOfUnitsDestroyed - previousTurnScores[s].numberOfUnitsDestroyed));
mail.params.put("basesDestroyed", Integer.toString(Math.max(scores[s].numberOfBasesDestroyed - previousTurnScores[s].numberOfBasesDestroyed, 0)));
mail.params.put("tilesConquered", Integer.toString(scores[s].numberOfTilesOwned - previousTurnScores[s].numberOfTilesOwned));
mail.params.put("link", GlobalProperties.getInstance().getString("app.url") + "/GamePlay.do" + "?gameId=" + id);
// Get sender address, the sender might not have filled in an e-mail address at all in which case sender.getEmailAsInternetAddress() will return null
InternetAddress senderAddress = new InternetAddress();
senderAddress.setPersonal(sender.getFriendlyName(), "UTF-8");
// Send mail
mail.sender = senderAddress;
mail.recipient = recipient.getEmailAsInternetAddress();
mail.templateJsp = templateJsp;
mail.title = title;
Mailer.getInstance().send(mail);
}
catch (Exception e)
{
Tools.logException(e);
}
}
/**
* Calculate time between two polls from the client
*
* @return Delta time in milliseconds
*/
public long getTimeBetweenPolls()
{
long time = Calendar.getInstance().getTime().getTime();
return Constants.MIN_DELAY_BETWEEN_CLIENT_POLLS + ((time - lastActionTime) * (Constants.MAX_DELAY_BETWEEN_CLIENT_POLLS - Constants.MIN_DELAY_BETWEEN_CLIENT_POLLS)) / GameStateCache.TIME_OUT;
}
/**
* Accumulate stats for an attack to put in the db at the end of the game
*
* @param attackerFaction Attacking faction
* @param attackerUnitConfigId Attacker unit config id
* @param defenderUnitConfigId Defender unit config id
* @param attackerDeaths Amount of units killed on attacking side (can be > 1 if unit contains other units)
* @param defenderDeaths Amount of units killed on defending side (can be > 1 if unit contains other units)
* @param attackerDamageDealt Amount of damage dealt by attacker
* @param defenderDamageDealt Amount of damage dealt by defender
*/
public void accumulateAttackStats(Faction attackerFaction, int attackerUnitConfigId, int defenderUnitConfigId, int attackerDeaths, int defenderDeaths, int attackerDamageDealt, int defenderDamageDealt)
{
int attackerInt = Faction.toInt(attackerFaction) - 1;
UnitStats au = unitStats[attackerInt].getOrCreate(attackerUnitConfigId);
UnitStats du = unitStats[attackerInt ^ 1].getOrCreate(defenderUnitConfigId);
au.numAttacks++;
du.numDefends++;
if (attackerDeaths > 0)
{
du.kills += attackerDeaths;
au.deaths += attackerDeaths;
}
if (defenderDeaths > 0)
{
au.kills += defenderDeaths;
du.deaths += defenderDeaths;
}
au.damageDealt += attackerDamageDealt;
du.damageDealt += defenderDamageDealt;
au.damageReceived += defenderDamageDealt;
du.damageReceived += attackerDamageDealt;
}
/**
* Update game state in database
*
* @throws SQLException
*/
public void updateDb() throws SQLException
{
if (needsToBeWrittenToDb)
{
GameStateDAO.update(this);
needsToBeWrittenToDb = false;
}
}
/**
* Clear cache for notification bar
*/
public void clearPageTopCache()
{
for (Player p : players)
if (p != null)
CacheTag.purgeElement("top", null, p.id);
}
}