package games.strategy.triplea.delegate; import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.Set; import games.strategy.engine.data.Change; import games.strategy.engine.data.CompositeChange; import games.strategy.engine.data.GameData; import games.strategy.engine.data.PlayerID; import games.strategy.engine.data.RelationshipType; import games.strategy.engine.data.Resource; import games.strategy.engine.data.Territory; import games.strategy.engine.data.changefactory.ChangeFactory; import games.strategy.engine.delegate.IDelegateBridge; import games.strategy.engine.random.IRandomStats.DiceType; import games.strategy.sound.SoundPath; import games.strategy.triplea.Constants; import games.strategy.triplea.MapSupport; import games.strategy.triplea.attachments.ICondition; import games.strategy.triplea.attachments.PoliticalActionAttachment; import games.strategy.triplea.attachments.RulesAttachment; import games.strategy.triplea.attachments.TriggerAttachment; import games.strategy.triplea.delegate.remote.IPoliticsDelegate; import games.strategy.triplea.formatter.MyFormatter; import games.strategy.triplea.ui.PoliticsText; import games.strategy.util.CompositeMatchAnd; import games.strategy.util.CompositeMatchOr; import games.strategy.util.Match; /** * Responsible allowing players to perform politicalActions. */ @MapSupport public class PoliticsDelegate extends BaseTripleADelegate implements IPoliticsDelegate { // protected HashMap<ICondition, Boolean> m_testedConditions = null; // private final boolean m_needToInitialize = true; /** Creates new PoliticsDelegate. */ public PoliticsDelegate() {} /** * Called before the delegate will run. */ @Override public void start() { super.start(); } @Override public void end() { super.end(); resetAttempts(); if (games.strategy.triplea.Properties.getTriggers(getData())) { // First set up a match for what we want to have fire as a default in this delegate. List out as a composite match // OR. // use 'null, null' because this is the Default firing location for any trigger that does NOT have 'when' set. final Match<TriggerAttachment> politicsDelegateTriggerMatch = new CompositeMatchAnd<>( TriggerAttachment.availableUses, TriggerAttachment.whenOrDefaultMatch(null, null), new CompositeMatchOr<TriggerAttachment>(TriggerAttachment.relationshipChangeMatch())); // get all possible triggers based on this match. final HashSet<TriggerAttachment> toFirePossible = TriggerAttachment.collectForAllTriggersMatching( new HashSet<>(Collections.singleton(m_player)), politicsDelegateTriggerMatch, m_bridge); if (!toFirePossible.isEmpty()) { // get all conditions possibly needed by these triggers, and then test them. final HashMap<ICondition, Boolean> testedConditions = TriggerAttachment.collectTestsForAllTriggers(toFirePossible, m_bridge); // get all triggers that are satisfied based on the tested conditions. final Set<TriggerAttachment> toFireTestedAndSatisfied = new HashSet<>( Match.getMatches(toFirePossible, TriggerAttachment.isSatisfiedMatch(testedConditions))); // now list out individual types to fire, once for each of the matches above. TriggerAttachment.triggerRelationshipChange(toFireTestedAndSatisfied, m_bridge, null, null, true, true, true, true); } } chainAlliancesTogether(m_bridge); givesBackOriginalTerritories(m_bridge); // m_needToInitialize = true; } @Override public Serializable saveState() { final PoliticsExtendedDelegateState state = new PoliticsExtendedDelegateState(); state.superState = super.saveState(); return state; } @Override public void loadState(final Serializable state) { final PoliticsExtendedDelegateState s = (PoliticsExtendedDelegateState) state; super.loadState(s.superState); } @Override public boolean delegateCurrentlyRequiresUserInput() { if (!m_player.amNotDeadYet(getData())) { return false; } if (!games.strategy.triplea.Properties.getUsePolitics(getData())) { return false; } return !getValidActions().isEmpty(); } public HashMap<ICondition, Boolean> getTestedConditions() { final HashSet<ICondition> allConditionsNeeded = RulesAttachment.getAllConditionsRecursive( new HashSet<>(PoliticalActionAttachment.getPoliticalActionAttachments(m_player)), null); return RulesAttachment.testAllConditionsRecursive(allConditionsNeeded, null, m_bridge); } @Override public Collection<PoliticalActionAttachment> getValidActions() { final GameData data = m_bridge.getData(); final HashMap<ICondition, Boolean> testedConditions; data.acquireReadLock(); try { testedConditions = getTestedConditions(); } finally { data.releaseReadLock(); } return PoliticalActionAttachment.getValidActions(m_player, testedConditions, data); } @Override public Class<IPoliticsDelegate> getRemoteType() { return IPoliticsDelegate.class; } @Override public void attemptAction(final PoliticalActionAttachment paa) { if (!games.strategy.triplea.Properties.getUsePolitics(getData())) { notifyPoliticsTurnedOff(); return; } if (paa.canPerform(getTestedConditions())) { if (checkEnoughMoney(paa)) { // See if the player has got enough money to pay for the action // Charge for attempting the action chargeForAction(paa); // take one of the uses this round paa.useAttempt(getBridge()); if (actionRollSucceeds(paa)) { // See if the action is successful if (actionIsAccepted(paa)) { // change the relationships changeRelationships(paa); // notify the players notifySuccess(paa); } else { // notify the players of the failed attempt notifyFailure(paa); } } else { // notify the players of the failed attempt notifyFailure(paa); } } else { // notify the player he hasn't got enough money; notifyMoney(paa, false); } } else { // notify the player the action isn't valid anymore (shouldn't happen) notifyNoValidAction(paa); } } /** * Get a list of players that should accept this action and then ask each * player if it accepts this action. * * @param paa * the politicalActionAttachment that should be accepted */ private boolean actionIsAccepted(final PoliticalActionAttachment paa) { final GameData data = getData(); final CompositeMatchOr<PoliticalActionAttachment> intoAlliedChainOrIntoOrOutOfWar = new CompositeMatchOr<>( Matches.politicalActionIsRelationshipChangeOf(null, Matches.RelationshipTypeIsAlliedAndAlliancesCanChainTogether.invert(), Matches.RelationshipTypeIsAlliedAndAlliancesCanChainTogether, data), Matches.politicalActionIsRelationshipChangeOf(null, Matches.RelationshipTypeIsAtWar.invert(), Matches.RelationshipTypeIsAtWar, data), Matches.politicalActionIsRelationshipChangeOf(null, Matches.RelationshipTypeIsAtWar, Matches.RelationshipTypeIsAtWar.invert(), data)); if (!games.strategy.triplea.Properties.getAlliancesCanChainTogether(data) || !intoAlliedChainOrIntoOrOutOfWar.match(paa)) { for (final PlayerID player : paa.getActionAccept()) { if (!(getRemotePlayer(player)).acceptAction(m_player, PoliticsText.getInstance().getAcceptanceQuestion(paa.getText()), true)) { return false; } } } else { // if alliances chain together, then our allies must have a say in anyone becoming a new ally/enemy final LinkedHashSet<PlayerID> playersWhoNeedToAccept = new LinkedHashSet<>(); playersWhoNeedToAccept.addAll(paa.getActionAccept()); playersWhoNeedToAccept.addAll(Match.getMatches(data.getPlayerList().getPlayers(), Matches.isAlliedAndAlliancesCanChainTogether(m_player, data))); for (final PlayerID player : paa.getActionAccept()) { playersWhoNeedToAccept.addAll(Match.getMatches(data.getPlayerList().getPlayers(), Matches.isAlliedAndAlliancesCanChainTogether(player, data))); } final HashSet<PlayerID> alliesWhoMustAccept = playersWhoNeedToAccept; alliesWhoMustAccept.removeAll(paa.getActionAccept()); for (final PlayerID player : playersWhoNeedToAccept) { String actionText = PoliticsText.getInstance().getAcceptanceQuestion(paa.getText()); if (actionText.equals("NONE")) { actionText = m_player.getName() + " wants to take the following action: " + MyFormatter.attachmentNameToText(paa.getName()) + " \r\n Do you approve?"; } else { actionText = m_player.getName() + " wants to take the following action: " + MyFormatter.attachmentNameToText(paa.getName()) + ". Do you approve? \r\n\r\n " + m_player.getName() + " will ask " + MyFormatter.defaultNamedToTextList(paa.getActionAccept()) + ", the following question: \r\n " + actionText; } if (!(getRemotePlayer(player)).acceptAction(m_player, actionText, true)) { return false; } } for (final PlayerID player : paa.getActionAccept()) { if (!(getRemotePlayer(player)).acceptAction(m_player, PoliticsText.getInstance().getAcceptanceQuestion(paa.getText()), true)) { return false; } } } return true; } /** * Let the player know this action isn't valid anymore, this shouldn't * happen as the player shouldn't get an option to push the button on * non-valid actions. */ private void notifyNoValidAction(final PoliticalActionAttachment paa) { sendNotification("This action isn't available anymore (this shouldn't happen!?!)"); } private void notifyPoliticsTurnedOff() { sendNotification("Politics is turned off in the game options"); } /** * Let the player know he is being charged for money or that he hasn't got * enough money * * @param paa * the actionattachment the player is notified about * @param enough * is this a notification about enough or not enough money. */ private void notifyMoney(final PoliticalActionAttachment paa, final boolean enough) { if (enough) { sendNotification("Charging " + paa.getCostPU() + " PU's to perform this action"); } else { sendNotification("You don't have ennough money, you need " + paa.getCostPU() + " PU's to perform this action"); } } /** * Subtract money from the players wallet * * @param paa * the politicalactionattachment this the money is charged for. */ private void chargeForAction(final PoliticalActionAttachment paa) { final Resource PUs = getData().getResourceList().getResource(Constants.PUS); final int cost = paa.getCostPU(); if (cost > 0) { // don't notify user of spending money anymore // notifyMoney(paa, true); final String transcriptText = m_bridge.getPlayerID().getName() + " spend " + cost + " PU on Political Action: " + MyFormatter.attachmentNameToText(paa.getName()); m_bridge.getHistoryWriter().startEvent(transcriptText); final Change charge = ChangeFactory.changeResourcesChange(m_bridge.getPlayerID(), PUs, -cost); m_bridge.addChange(charge); } else { final String transcriptText = m_bridge.getPlayerID().getName() + " takes Political Action: " + MyFormatter.attachmentNameToText(paa.getName()); // we must start an event anyway m_bridge.getHistoryWriter().startEvent(transcriptText); } } /** * @param paa * The Political Action the player should be charged for. * @return false if the player can't afford the action */ private boolean checkEnoughMoney(final PoliticalActionAttachment paa) { final Resource PUs = getData().getResourceList().getResource(Constants.PUS); final int cost = paa.getCostPU(); final int has = m_bridge.getPlayerID().getResources().getQuantity(PUs); return has >= cost; } /** * Let all players involved in this action know the action has failed. * * @param paa * the political action attachment that just failed. */ private void notifyFailure(final PoliticalActionAttachment paa) { getSoundChannel().playSoundForAll(SoundPath.CLIP_POLITICAL_ACTION_FAILURE, m_player); final String transcriptText = m_bridge.getPlayerID().getName() + " fails on action: " + MyFormatter.attachmentNameToText(paa.getName()); m_bridge.getHistoryWriter().addChildToEvent(transcriptText); sendNotification(PoliticsText.getInstance().getNotificationFailure(paa.getText())); notifyOtherPlayers(PoliticsText.getInstance().getNotificationFailureOthers(paa.getText())); } /** * Let all players involved in this action know the action was successful * * @param paa the political action attachment that just succeeded. */ private void notifySuccess(final PoliticalActionAttachment paa) { getSoundChannel().playSoundForAll(SoundPath.CLIP_POLITICAL_ACTION_SUCCESSFUL, m_player); sendNotification(PoliticsText.getInstance().getNotificationSucccess(paa.getText())); notifyOtherPlayers(PoliticsText.getInstance().getNotificationSuccessOthers(paa.getText())); } /** * Send a notification to the other players involved in this action (all * players except the player starting the action). */ private void notifyOtherPlayers(final String notification) { if (!"NONE".equals(notification)) { // we can send it to just paa.getOtherPlayers(), or we can send it to all players. both are good options. final Collection<PlayerID> currentPlayer = new ArrayList<>(); currentPlayer.add(m_player); final Collection<PlayerID> otherPlayers = getData().getPlayerList().getPlayers(); otherPlayers.removeAll(currentPlayer); this.getDisplay().reportMessageToPlayers(otherPlayers, currentPlayer, notification, notification); } } /** * Send a notification to the current player. * * @param text * if NONE don't send a notification */ private void sendNotification(final String text) { if (!"NONE".equals(text)) { this.getRemotePlayer().reportMessage(text, text); } } /** * Changes all relationships. * * @param paa * the political action to change the relationships for */ private void changeRelationships(final PoliticalActionAttachment paa) { getMyselfOutOfAlliance(paa, m_player, m_bridge); getNeutralOutOfWarWithAllies(paa, m_player, m_bridge); final CompositeChange change = new CompositeChange(); for (final String relationshipChange : paa.getRelationshipChange()) { final String[] s = relationshipChange.split(":"); final PlayerID player1 = getData().getPlayerList().getPlayerID(s[0]); final PlayerID player2 = getData().getPlayerList().getPlayerID(s[1]); final RelationshipType oldRelation = getData().getRelationshipTracker().getRelationshipType(player1, player2); final RelationshipType newRelation = getData().getRelationshipTypeList().getRelationshipType(s[2]); if (oldRelation.equals(newRelation)) { continue; } change.add(ChangeFactory.relationshipChange(player1, player2, oldRelation, newRelation)); m_bridge.getHistoryWriter() .addChildToEvent(m_bridge.getPlayerID().getName() + " succeeds on action: " + MyFormatter.attachmentNameToText(paa.getName()) + ": Changing Relationship for " + player1.getName() + " and " + player2.getName() + " from " + oldRelation.getName() + " to " + newRelation.getName()); MoveDelegate.getBattleTracker(getData()).addRelationshipChangesThisTurn(player1, player2, oldRelation, newRelation); /* * creation of new battles is handled at the beginning of the battle delegate, in * "setupUnitsInSameTerritoryBattles", not here. * if (Matches.RelationshipTypeIsAtWar.match(newRelation)) * TriggerAttachment.triggerMustFightBattle(player1, player2, m_bridge); */ } if (!change.isEmpty()) { m_bridge.addChange(change); } chainAlliancesTogether(m_bridge); } /** * @param paa * the action to check if it succeeds * @return true if the action succeeds, usually because the die-roll succeeded. */ private boolean actionRollSucceeds(final PoliticalActionAttachment paa) { final int hitTarget = paa.getChanceToHit(); final int diceSides = paa.getChanceDiceSides(); if (diceSides <= 0 || hitTarget >= diceSides) { paa.changeChanceDecrementOrIncrementOnSuccessOrFailure(m_bridge, true, true); return true; } else if (hitTarget <= 0) { paa.changeChanceDecrementOrIncrementOnSuccessOrFailure(m_bridge, false, true); return false; } final int rollResult = m_bridge.getRandom(diceSides, m_player, DiceType.NONCOMBAT, "Attempting the Political Action: " + MyFormatter.attachmentNameToText(paa.getName())) + 1; final boolean success = rollResult <= hitTarget; final String notificationMessage = "rolling (" + hitTarget + " out of " + diceSides + ") result: " + rollResult + " = " + (success ? "Success!" : "Failure!"); m_bridge.getHistoryWriter() .addChildToEvent(MyFormatter.attachmentNameToText(paa.getName()) + " : " + notificationMessage); paa.changeChanceDecrementOrIncrementOnSuccessOrFailure(m_bridge, success, true); sendNotification(notificationMessage); return success; } /** * Reset the attempts-counter for this action, so next round the player can * try again for a number of attempts. */ private void resetAttempts() { for (final PoliticalActionAttachment paa : PoliticalActionAttachment.getPoliticalActionAttachments(m_player)) { paa.resetAttempts(getBridge()); } } private static void getMyselfOutOfAlliance(final PoliticalActionAttachment paa, final PlayerID player, final IDelegateBridge aBridge) { final GameData data = aBridge.getData(); if (!games.strategy.triplea.Properties.getAlliancesCanChainTogether(data)) { return; } final Collection<PlayerID> players = data.getPlayerList().getPlayers(); final Collection<PlayerID> p1AlliedWith = Match.getMatches(players, Matches.isAlliedAndAlliancesCanChainTogether(player, data)); p1AlliedWith.remove(player); final CompositeChange change = new CompositeChange(); for (final String relationshipChangeString : paa.getRelationshipChange()) { final String[] relationshipChange = relationshipChangeString.split(":"); final PlayerID p1 = data.getPlayerList().getPlayerID(relationshipChange[0]); final PlayerID p2 = data.getPlayerList().getPlayerID(relationshipChange[1]); if (!(p1.equals(player) || p2.equals(player))) { continue; } final PlayerID pOther = (p1.equals(player) ? p2 : p1); if (!p1AlliedWith.contains(pOther)) { continue; } final RelationshipType currentType = data.getRelationshipTracker().getRelationshipType(p1, p2); final RelationshipType newType = data.getRelationshipTypeList().getRelationshipType(relationshipChange[2]); if (Matches.RelationshipTypeIsAlliedAndAlliancesCanChainTogether.match(currentType) && Matches.RelationshipTypeIsAlliedAndAlliancesCanChainTogether.invert().match(newType)) { for (final PlayerID p3 : p1AlliedWith) { final RelationshipType currentOther = data.getRelationshipTracker().getRelationshipType(p3, player); if (!currentOther.equals(newType)) { change.add(ChangeFactory.relationshipChange(p3, player, currentOther, newType)); aBridge.getHistoryWriter().addChildToEvent( player.getName() + " and " + p3.getName() + " sign a " + newType.getName() + " treaty"); MoveDelegate.getBattleTracker(data).addRelationshipChangesThisTurn(p3, player, currentOther, newType); } } } } if (!change.isEmpty()) { aBridge.addChange(change); } } private static void getNeutralOutOfWarWithAllies(final PoliticalActionAttachment paa, final PlayerID player, final IDelegateBridge aBridge) { final GameData data = aBridge.getData(); if (!games.strategy.triplea.Properties.getAlliancesCanChainTogether(data)) { return; } final Collection<PlayerID> players = data.getPlayerList().getPlayers(); final Collection<PlayerID> p1AlliedWith = Match.getMatches(players, Matches.isAlliedAndAlliancesCanChainTogether(player, data)); final CompositeChange change = new CompositeChange(); for (final String relationshipChangeString : paa.getRelationshipChange()) { final String[] relationshipChange = relationshipChangeString.split(":"); final PlayerID p1 = data.getPlayerList().getPlayerID(relationshipChange[0]); final PlayerID p2 = data.getPlayerList().getPlayerID(relationshipChange[1]); if (!(p1.equals(player) || p2.equals(player))) { continue; } final PlayerID pOther = (p1.equals(player) ? p2 : p1); final RelationshipType currentType = data.getRelationshipTracker().getRelationshipType(p1, p2); final RelationshipType newType = data.getRelationshipTypeList().getRelationshipType(relationshipChange[2]); if (Matches.RelationshipTypeIsAtWar.match(currentType) && Matches.RelationshipTypeIsAtWar.invert().match(newType)) { final Collection<PlayerID> pOtherAlliedWith = Match.getMatches(players, Matches.isAlliedAndAlliancesCanChainTogether(pOther, data)); if (!pOtherAlliedWith.contains(pOther)) { pOtherAlliedWith.add(pOther); } if (!p1AlliedWith.contains(player)) { p1AlliedWith.add(player); } for (final PlayerID p3 : p1AlliedWith) { for (final PlayerID p4 : pOtherAlliedWith) { final RelationshipType currentOther = data.getRelationshipTracker().getRelationshipType(p3, p4); if (!currentOther.equals(newType) && Matches.RelationshipTypeIsAtWar.match(currentOther)) { change.add(ChangeFactory.relationshipChange(p3, p4, currentOther, newType)); aBridge.getHistoryWriter() .addChildToEvent(p3.getName() + " and " + p4.getName() + " sign a " + newType.getName() + " treaty"); MoveDelegate.getBattleTracker(data).addRelationshipChangesThisTurn(p3, p4, currentOther, newType); } } } } } if (!change.isEmpty()) { aBridge.addChange(change); } } public static void chainAlliancesTogether(final IDelegateBridge aBridge) { final GameData data = aBridge.getData(); if (!games.strategy.triplea.Properties.getAlliancesCanChainTogether(data)) { return; } final Collection<RelationshipType> allTypes = data.getRelationshipTypeList().getAllRelationshipTypes(); RelationshipType alliedType = null; RelationshipType warType = null; for (final RelationshipType type : allTypes) { if (type.getRelationshipTypeAttachment().getIsDefaultWarPosition()) { warType = type; } else if (type.getRelationshipTypeAttachment().getAlliancesCanChainTogether()) { alliedType = type; } } if (alliedType == null) { return; } // first do alliances. then, do war (since we don't want to declare war on a potential ally). final Collection<PlayerID> players = data.getPlayerList().getPlayers(); for (final PlayerID p1 : players) { final HashSet<PlayerID> p1NewAllies = new HashSet<>(); final Collection<PlayerID> p1AlliedWith = Match.getMatches(players, Matches.isAlliedAndAlliancesCanChainTogether(p1, data)); for (final PlayerID p2 : p1AlliedWith) { p1NewAllies.addAll(Match.getMatches(players, Matches.isAlliedAndAlliancesCanChainTogether(p2, data))); } p1NewAllies.removeAll(p1AlliedWith); p1NewAllies.remove(p1); for (final PlayerID p3 : p1NewAllies) { if (!data.getRelationshipTracker().getRelationshipType(p1, p3).equals(alliedType)) { final RelationshipType current = data.getRelationshipTracker().getRelationshipType(p1, p3); aBridge.addChange(ChangeFactory.relationshipChange(p1, p3, current, alliedType)); aBridge.getHistoryWriter().addChildToEvent( p1.getName() + " and " + p3.getName() + " are joined together in an " + alliedType.getName() + " treaty"); MoveDelegate.getBattleTracker(data).addRelationshipChangesThisTurn(p1, p3, current, alliedType); } } } // now war if (warType == null) { return; } for (final PlayerID p1 : players) { final HashSet<PlayerID> p1NewWar = new HashSet<>(); final Collection<PlayerID> p1WarWith = Match.getMatches(players, Matches.isAtWar(p1, data)); final Collection<PlayerID> p1AlliedWith = Match.getMatches(players, Matches.isAlliedAndAlliancesCanChainTogether(p1, data)); for (final PlayerID p2 : p1AlliedWith) { p1NewWar.addAll(Match.getMatches(players, Matches.isAtWar(p2, data))); } p1NewWar.removeAll(p1WarWith); p1NewWar.remove(p1); for (final PlayerID p3 : p1NewWar) { if (!data.getRelationshipTracker().getRelationshipType(p1, p3).equals(warType)) { final RelationshipType current = data.getRelationshipTracker().getRelationshipType(p1, p3); aBridge.addChange(ChangeFactory.relationshipChange(p1, p3, current, warType)); aBridge.getHistoryWriter().addChildToEvent( p1.getName() + " and " + p3.getName() + " declare " + warType.getName() + " on each other"); MoveDelegate.getBattleTracker(data).addRelationshipChangesThisTurn(p1, p3, current, warType); } } } } public static void givesBackOriginalTerritories(final IDelegateBridge aBridge) { final GameData data = aBridge.getData(); final CompositeChange change = new CompositeChange(); final Collection<PlayerID> players = data.getPlayerList().getPlayers(); for (final PlayerID p1 : players) { for (final PlayerID p2 : players) { if (!data.getRelationshipTracker().givesBackOriginalTerritories(p1, p2)) { continue; } for (final Territory t : data.getMap().getTerritoriesOwnedBy(p1)) { final PlayerID original = OriginalOwnerTracker.getOriginalOwner(t); if (original == null) { continue; } if (original.equals(p2)) { change.add(ChangeFactory.changeOwner(t, original)); } } } } if (!change.isEmpty()) { aBridge.getHistoryWriter().startEvent("Giving back territories to original owners"); aBridge.addChange(change); } } } class PoliticsExtendedDelegateState implements Serializable { private static final long serialVersionUID = -3829812751864156598L; Serializable superState; // add other variables here: // public HashMap<ICondition, Boolean> m_testedConditions = null; }