From: Erik V. <ev...@us...> - 2011-08-11 22:16:26
|
rails/game/OperatingRound.java | 4163 ++++++++++++++++++++--------------------- 1 file changed, 2110 insertions(+), 2053 deletions(-) New commits: commit 8d9ca907cc522935c710a8349d13a9f97a50eebf Author: Erik Vos <eri...@xs...> Date: Fri Aug 12 00:14:14 2011 +0200 Reorganized OperatingRound No code changes, but methods have been sorted into functional groups, each preceded by a header. Hopefully this class will now be easier to grasp. diff --git a/rails/game/OperatingRound.java b/rails/game/OperatingRound.java index 957203e..ec8de9c 100644 --- a/rails/game/OperatingRound.java +++ b/rails/game/OperatingRound.java @@ -3,9 +3,7 @@ package rails.game; import java.util.*; -import rails.common.DisplayBuffer; -import rails.common.GuiDef; -import rails.common.LocalText; +import rails.common.*; import rails.common.parser.GameOption; import rails.game.action.*; import rails.game.correct.ClosePrivate; @@ -82,9 +80,12 @@ public class OperatingRound extends Round implements Observer { protected TrainManager trainManager = gameManager.getTrainManager(); + /*======================================= + * 1. OR START and END + *=======================================*/ + /** * Constructor with no parameters, call the super Class (Round's) Constructor with no parameters - * */ public OperatingRound(GameManagerI gameManager) { super (gameManager); @@ -158,7 +159,56 @@ public class OperatingRound extends Round implements Observer { } - /*----- METHODS THAT PROCESS PLAYER ACTIONS -----*/ + @Override + public void resume() { + + if (savedAction instanceof BuyTrain) { + buyTrain ((BuyTrain)savedAction); + } else if (savedAction instanceof SetDividend) { + executeSetRevenueAndDividend ((SetDividend) savedAction); + } else if (savedAction instanceof RepayLoans) { + executeRepayLoans ((RepayLoans) savedAction); + } else if (savedAction == null) { + //nextStep(); + } + savedAction = null; + wasInterrupted.set(true); + + guiHints.setVisibilityHint(GuiDef.Panel.STOCK_MARKET, false); + guiHints.setVisibilityHint(GuiDef.Panel.STATUS, true); + guiHints.setActivePanel(GuiDef.Panel.MAP); + } + + protected void finishOR() { + + // Check if any privates must be closed + // (now only applies to 1856 W&SR) - no, that is at end of TURN + //for (PrivateCompanyI priv : gameManager.getAllPrivateCompanies()) { + // priv.checkClosingIfExercised(true); + //} + + ReportBuffer.add(" "); + ReportBuffer.add(LocalText.getText("EndOfOperatingRound", thisOrNumber)); + + // Update the worth increase per player + int orWorthIncrease; + for (Player player : gameManager.getPlayers()) { + player.setLastORWorthIncrease(); + orWorthIncrease = player.getLastORWorthIncrease().intValue(); + ReportBuffer.add(LocalText.getText("ORWorthIncrease", + player.getName(), + thisOrNumber, + Bank.format(orWorthIncrease))); + } + + // OR done. Inform GameManager. + finishRound(); + } + + /*======================================= + * 2. CENTRAL PROCESSING FUNCTIONS + * 2.1. PROCESS USER ACTION + *=======================================*/ @Override public boolean process(PossibleAction action) { @@ -236,10 +286,10 @@ public class OperatingRound extends Round implements Observer { } else if (selectedAction instanceof ClosePrivate) { result = executeClosePrivate((ClosePrivate)selectedAction); - + } else if (selectedAction instanceof UseSpecialProperty && ((UseSpecialProperty)selectedAction).getSpecialProperty() instanceof SpecialRight) { - + result = buyRight ((UseSpecialProperty)selectedAction); } else if (selectedAction instanceof NullAction) { @@ -275,1073 +325,1284 @@ public class OperatingRound extends Round implements Observer { return false; } - public boolean layTile(LayTile action) { + /*======================================= + * 2.2. PREPARE NEXT ACTION + *=======================================*/ - String errMsg = null; - int cost = 0; - SpecialTileLay stl = null; - boolean extra = false; + /** + * To be called after each change, to re-establish the currently allowed + * actions. (new method, intended to absorb code from several other + * methods). + * + */ + @Override + public boolean setPossibleActions() { - PublicCompanyI company = action.getCompany(); - String companyName = company.getName(); - TileI tile = action.getLaidTile(); - MapHex hex = action.getChosenHex(); - int orientation = action.getOrientation(); + /* Create a new list of possible actions for the UI */ + possibleActions.clear(); + selectedAction = null; - // Dummy loop to enable a quick jump out. - while (true) { - // Checks - // Must be correct company. - if (!companyName.equals(operatingCompany.get().getName())) { - errMsg = - LocalText.getText("WrongCompany", - companyName, - operatingCompany.get().getName() ); - break; - } - // Must be correct step - if (getStep() != GameDef.OrStep.LAY_TRACK) { - errMsg = LocalText.getText("WrongActionNoTileLay"); - break; + boolean forced = false; + doneAllowed = false; // set default (fix of bug 2954654) + + if (getStep() == GameDef.OrStep.INITIAL) { + initTurn(); + if (noMapMode) { + nextStep (GameDef.OrStep.LAY_TOKEN); + } else { + initNormalTileLays(); // new: only called once per turn ? + setStep (GameDef.OrStep.LAY_TRACK); } + } - if (tile == null) break; + GameDef.OrStep step = getStep(); + if (step == GameDef.OrStep.LAY_TRACK) { - if (!getCurrentPhase().isTileColourAllowed(tile.getColourName())) { - errMsg = - LocalText.getText("TileNotYetAvailable", - tile.getExternalId()); - break; - } - if (tile.countFreeTiles() == 0) { - errMsg = - LocalText.getText("TileNotAvailable", - tile.getExternalId()); - break; + if (!operatingCompany.get().hasLaidHomeBaseTokens()) { + // This can occur if the home hex has two cities and track, + // such as the green OO tile #59 + possibleActions.add(new LayBaseToken (operatingCompany.get().getHomeHexes())); + forced = true; + } else { + possibleActions.addAll(getNormalTileLays(true)); + possibleActions.addAll(getSpecialTileLays(true)); + possibleActions.add(new NullAction(NullAction.SKIP)); } - /* - * Check if the current tile is allowed via the LayTile allowance. - * (currently the set if tiles is always null, which means that this - * check is redundant. This may change in the future. - */ - if (action != null) { - List<TileI> tiles = action.getTiles(); - if (tiles != null && !tiles.isEmpty() && !tiles.contains(tile)) { - errMsg = - LocalText.getText( - "TileMayNotBeLaidInHex", - tile.getExternalId(), - hex.getName() ); - break; - } - stl = action.getSpecialProperty(); - if (stl != null) extra = stl.isExtra(); - } + } else if (step == GameDef.OrStep.LAY_TOKEN) { + setNormalTokenLays(); + setSpecialTokenLays(); + log.debug("Normal token lays: " + currentNormalTokenLays.size()); + log.debug("Special token lays: " + currentSpecialTokenLays.size()); - /* - * If this counts as a normal tile lay, check if the allowed number - * of normal tile lays is not exceeded. - */ - if (!extra && !validateNormalTileLay(tile)) { - errMsg = - LocalText.getText("NumberOfNormalTileLaysExceeded", - tile.getColourName()); - break; - } + possibleActions.addAll(currentNormalTokenLays); + possibleActions.addAll(currentSpecialTokenLays); + possibleActions.add(new NullAction(NullAction.SKIP)); + } else if (step == GameDef.OrStep.CALC_REVENUE) { + prepareRevenueAndDividendAction(); + if (noMapMode) + prepareNoMapActions(); + } else if (step == GameDef.OrStep.BUY_TRAIN) { + setBuyableTrains(); + // TODO Need route checking here. + // TEMPORARILY allow not buying a train if none owned + //if (!operatingCompany.getObject().mustOwnATrain() + // || operatingCompany.getObject().getPortfolio().getNumberOfTrains() > 0) { + doneAllowed = true; + //} + if (noMapMode && (operatingCompany.get().getLastRevenue() == 0)) + prepareNoMapActions(); - // Sort out cost - if (stl != null && stl.isFree()) { - cost = 0; - } else { - cost = hex.getTileCost(); - } + } else if (step == GameDef.OrStep.DISCARD_TRAINS) { - // Amount must be non-negative multiple of 10 - if (cost < 0) { - errMsg = - LocalText.getText("NegativeAmountNotAllowed", - Bank.format(cost)); - break; - } - if (cost % 10 != 0) { - errMsg = - LocalText.getText("AmountMustBeMultipleOf10", - Bank.format(cost)); - break; - } - // Does the company have the money? - if (cost > operatingCompany.get().getCash()) { - errMsg = - LocalText.getText("NotEnoughMoney", - companyName, - Bank.format(operatingCompany.get().getCash()), - Bank.format(cost) ); - break; - } - break; - } - if (errMsg != null) { - DisplayBuffer.add(LocalText.getText("CannotLayTileOn", - companyName, - tile.getExternalId(), - hex.getName(), - Bank.format(cost), - errMsg )); - return false; + forced = true; + setTrainsToDiscard(); } - /* End of validation, start of execution */ - moveStack.start(true); + // The following additional "common" actions are only available if the + // primary action is not forced. + if (!forced) { - if (tile != null) { - if (cost > 0) - new CashMove(operatingCompany.get(), bank, cost); - operatingCompany.get().layTile(hex, tile, orientation, cost); + setBonusTokenLays(); - if (cost == 0) { - ReportBuffer.add(LocalText.getText("LaysTileAt", - companyName, - tile.getExternalId(), - hex.getName(), - hex.getOrientationName(orientation))); - } else { - ReportBuffer.add(LocalText.getText("LaysTileAtFor", - companyName, - tile.getExternalId(), - hex.getName(), - hex.getOrientationName(orientation), - Bank.format(cost) )); - } - hex.upgrade(action); + setDestinationActions(); - // Was a special property used? - if (stl != null) { - stl.setExercised(); - //currentSpecialTileLays.remove(action); - log.debug("This was a special tile lay, " - + (extra ? "" : " not") + " extra"); + setGameSpecificPossibleActions(); + // Private Company manually closure + for (PrivateCompanyI priv: companyManager.getAllPrivateCompanies()) { + if (!priv.isClosed() && priv.closesManually()) + possibleActions.add(new ClosePrivate(priv)); } - if (!extra) { - log.debug("This was a normal tile lay"); - registerNormalTileLay(tile); - } - } - if (tile == null || !areTileLaysPossible()) { - nextStep(); - } - - return true; - } + // Can private companies be bought? + if (isPrivateSellingAllowed()) { - protected boolean validateNormalTileLay(TileI tile) { - return checkNormalTileLay(tile, false); - } + // Create a list of players with the current one in front + int currentPlayerIndex = operatingCompany.get().getPresident().getIndex(); + Player player; + int minPrice, maxPrice; + List<Player> players = getPlayers(); + int numberOfPlayers = getNumberOfPlayers(); + for (int i = currentPlayerIndex; i < currentPlayerIndex + + numberOfPlayers; i++) { + player = players.get(i % numberOfPlayers); + if (!maySellPrivate(player)) continue; + for (PrivateCompanyI privComp : player.getPortfolio().getPrivateCompanies()) { - protected void registerNormalTileLay(TileI tile) { - checkNormalTileLay(tile, true); - } + // check to see if the private can be sold to a company + if (!privComp.tradeableToCompany()) { + continue; + } - protected boolean checkNormalTileLay(TileI tile, boolean update) { + minPrice = getPrivateMinimumPrice (privComp); - // Unspecified tile (e.g. 1889 D private, which is free on mountains) - if (tile == null) { - return !tileLaysPerColour.isEmpty(); - } - - String colour = tile.getColourName(); - Integer oldAllowedNumberObject = tileLaysPerColour.get(colour); + maxPrice = getPrivateMaximumPrice (privComp); - if (oldAllowedNumberObject == null) return false; + possibleActions.add(new BuyPrivate(privComp, minPrice, + maxPrice)); + } + } + } - int oldAllowedNumber = oldAllowedNumberObject.intValue(); - if (oldAllowedNumber <= 0) return false; + if (operatingCompany.get().canUseSpecialProperties()) { - if (update) updateAllowedTileColours(colour, oldAllowedNumber); - return true; - } + // Are there any "common" special properties, + // i.e. properties that are available to everyone? + List<SpecialPropertyI> commonSP = gameManager.getCommonSpecialProperties(); + if (commonSP != null) { + SellBonusToken sbt; + loop: for (SpecialPropertyI sp : commonSP) { + if (sp instanceof SellBonusToken) { + sbt = (SellBonusToken) sp; + // Can't buy if already owned + if (operatingCompany.get().getBonuses() != null) { + for (Bonus bonus : operatingCompany.get().getBonuses()) { + if (bonus.getName().equals(sp.getName())) continue loop; + } + } + possibleActions.add (new BuyBonusToken (sbt)); + } + } + } - /* - * We will assume that in all cases the following assertions hold: 1. If - * the allowed number for the colour of the just laid tile reaches zero, - * all normal tile lays have been consumed. 2. If any colour is laid, no - * different colours may be laid. THIS MAY NOT BE TRUE FOR ALL GAMES! - */ + // Are there other step-independent special properties owned by the company? + List<SpecialPropertyI> orsps = operatingCompany.get().getPortfolio().getAllSpecialProperties(); + List<SpecialPropertyI> compsps = operatingCompany.get().getSpecialProperties(); + if (compsps != null) orsps.addAll(compsps); - protected void updateAllowedTileColours (String colour, int oldAllowedNumber) { - - if (oldAllowedNumber <= 1) { - tileLaysPerColour.clear(); - log.debug("No more normal tile lays allowed"); - //currentNormalTileLays.clear();// Shouldn't be needed anymore ?? - } else { - List<String> coloursToRemove = new ArrayList<String>(); - for (String key:tileLaysPerColour.viewKeySet()) { - if (colour.equals(key)) { - tileLaysPerColour.put(key, oldAllowedNumber-1); - } else { - coloursToRemove.add(key); + if (orsps != null) { + for (SpecialPropertyI sp : orsps) { + if (!sp.isExercised() && sp.isUsableIfOwnedByCompany() + && sp.isUsableDuringOR(step)) { + if (sp instanceof SpecialTokenLay) { + if (getStep() != GameDef.OrStep.LAY_TOKEN) { + possibleActions.add(new LayBaseToken((SpecialTokenLay)sp)); + } + } else { + possibleActions.add(new UseSpecialProperty(sp)); + } + } + } + } + // Are there other step-independent special properties owned by the president? + orsps = getCurrentPlayer().getPortfolio().getAllSpecialProperties(); + if (orsps != null) { + for (SpecialPropertyI sp : orsps) { + if (!sp.isExercised() && sp.isUsableIfOwnedByPlayer() + && sp.isUsableDuringOR(step)) { + if (sp instanceof SpecialTokenLay) { + if (getStep() != GameDef.OrStep.LAY_TOKEN) { + possibleActions.add(new LayBaseToken((SpecialTokenLay)sp)); + } + } else { + possibleActions.add(new UseSpecialProperty(sp)); + } + } + } } } - // Two-step removal to prevent ConcurrentModificatioonException. - for (String key : coloursToRemove) { - tileLaysPerColour.remove(key); - } - log.debug((oldAllowedNumber - 1) + " additional " + colour - + " tile lays allowed; no other colours"); } - } - public boolean layBaseToken(LayBaseToken action) { + if (doneAllowed) { + possibleActions.add(new NullAction(NullAction.DONE)); + } - String errMsg = null; - int cost = 0; - SpecialTokenLay stl = null; - boolean extra = false; + for (PossibleAction pa : possibleActions.getList()) { + try { + log.debug(operatingCompany.get().getName() + " may: " + pa.toString()); + } catch (Exception e) { + log.error("Error in toString() of " + pa.getClass(), e); + } + } - MapHex hex = action.getChosenHex(); - int station = action.getChosenStation(); - String companyName = operatingCompany.get().getName(); + return true; + } - // TEMPORARY FIX to enable fixing invalidated saved files - //if ("N11".equals(hex.getName()) && station == 2) { - // station = 1; - // action.setChosenStation(1); - //} + /** Stub, can be overridden by subclasses */ + protected void setGameSpecificPossibleActions() { - // Dummy loop to enable a quick jump out. - while (true) { + } - // Checks - // Must be correct step (exception: home base lay & some special token lay) - if (getStep() != GameDef.OrStep.LAY_TOKEN - && action.getType() != LayBaseToken.HOME_CITY - && action.getType() != LayBaseToken.SPECIAL_PROPERTY) { - errMsg = LocalText.getText("WrongActionNoTokenLay"); - break; - } + /*======================================= + * 2.3. TURN CONTROL + *=======================================*/ - if (operatingCompany.get().getNumberOfFreeBaseTokens() == 0) { - errMsg = LocalText.getText("HasNoTokensLeft", companyName); - break; - } + protected void initTurn() { + log.debug("Starting turn of "+operatingCompany.get().getName()); + ReportBuffer.add(" "); + ReportBuffer.add(LocalText.getText("CompanyOperates", + operatingCompany.get().getName(), + operatingCompany.get().getPresident().getName())); + setCurrentPlayer(operatingCompany.get().getPresident()); - if (!isTokenLayAllowed (operatingCompany.get(), hex, station)) { - errMsg = LocalText.getText("BaseTokenSlotIsReserved"); - break; + if (noMapMode && !operatingCompany.get().hasLaidHomeBaseTokens()){ + // Lay base token in noMapMode + BaseToken token = operatingCompany.get().getFreeToken(); + if (token == null) { + log.error("Company " + operatingCompany.get().getName() + " has no free token to lay base token"); + } else { + log.debug("Company " + operatingCompany.get().getName() + " lays base token in nomap mode"); + token.moveTo(bank.getUnavailable()); } + } + operatingCompany.get().initTurn(); + trainsBoughtThisTurn.clear(); + } - if (!hex.hasTokenSlotsLeft(station)) { - errMsg = LocalText.getText("CityHasNoEmptySlots"); - break; - } + protected void finishTurn() { - /* - * TODO: the below condition holds for 1830. in some games, separate - * cities on one tile may hold tokens of the same company; this case - * is not yet covered. - */ - if (hex.hasTokenOfCompany(operatingCompany.get())) { - errMsg = - LocalText.getText("TileAlreadyHasToken", - hex.getName(), - companyName ); - break; - } + if (!operatingCompany.get().isClosed()) { + operatingCompany.get().setOperated(); + companiesOperatedThisRound.add(operatingCompany.get()); - if (action != null) { - List<MapHex> locations = action.getLocations(); - if (locations != null && locations.size() > 0 - && !locations.contains(hex) && !locations.contains(null)) { - errMsg = - LocalText.getText("TokenLayingHexMismatch", - hex.getName(), - action.getLocationNameString() ); - break; - } - stl = action.getSpecialProperty(); - if (stl != null) extra = stl.isExtra(); + // Check if any privates must be closed (now only applies to 1856 W&SR) + // Copy list first to avoid concurrent modifications + for (PrivateCompanyI priv : + new ArrayList<PrivateCompanyI> (operatingCompany.get().getPortfolio().getPrivateCompanies())) { + priv.checkClosingIfExercised(true); } + } - cost = operatingCompany.get().getBaseTokenLayCost(hex); - if (stl != null && stl.isFree()) cost = 0; + if (!finishTurnSpecials()) return; - // Does the company have the money? - if (cost > operatingCompany.get().getCash()) { - errMsg = LocalText.getText("NotEnoughMoney", - companyName, - Bank.format(operatingCompany.get().getCash()), - Bank.format(cost)); - break; - } - break; - } - if (errMsg != null) { - DisplayBuffer.add(LocalText.getText("CannotLayBaseTokenOn", - companyName, - hex.getName(), - Bank.format(cost), - errMsg )); - return false; + if (setNextOperatingCompany(false)) { + setStep(GameDef.OrStep.INITIAL); + } else { + finishOR(); } + } - /* End of validation, start of execution */ - moveStack.start(true); + /** Stub, may be overridden in subclasses + * Return value: + * TRUE = normal turn end; + * FALSE = return immediately from finishTurn(). + */ + protected boolean finishTurnSpecials () { + return true; + } - if (hex.layBaseToken(operatingCompany.get(), station)) { - /* TODO: the false return value must be impossible. */ + protected boolean setNextOperatingCompany(boolean initial) { - operatingCompany.get().layBaseToken(hex, cost); - - // If this is a home base token lay, stop here - if (action.getType() == LayBaseToken.HOME_CITY) { - return true; - } - - if (cost > 0) { - new CashMove(operatingCompany.get(), bank, cost); - ReportBuffer.add(LocalText.getText("LAYS_TOKEN_ON", - companyName, - hex.getName(), - Bank.format(cost) )); + while (true) { + if (initial || operatingCompany.get() == null || operatingCompany == null) { + setOperatingCompany(operatingCompanies.get(0)); + initial = false; } else { - ReportBuffer.add(LocalText.getText("LAYS_FREE_TOKEN_ON", - companyName, - hex.getName() )); - } + int index = operatingCompanies.indexOf(operatingCompany.get()); + if (++index >= operatingCompanies.size()) { + return false; + } - // Was a special property used? - if (stl != null) { - stl.setExercised(); - currentSpecialTokenLays.remove(action); - log.debug("This was a special token lay, " - + (extra ? "" : " not") + " extra"); + // Check if the operating order has changed + List<PublicCompanyI> newOperatingCompanies + = setOperatingCompanies (operatingCompanies.viewList(), operatingCompany.get()); + PublicCompanyI company; + for (int i=0; i<newOperatingCompanies.size(); i++) { + company = newOperatingCompanies.get(i); + if (company != operatingCompanies.get(i)) { + log.debug("Company "+company.getName() + +" replaces "+operatingCompanies.get(i).getName() + +" in operating sequence"); + operatingCompanies.move(company, i); + } + } + setOperatingCompany(operatingCompanies.get(index)); } - // Jump out if we aren't in the token laying step - if (getStep() != GameDef.OrStep.LAY_TOKEN) return true; - - if (!extra) { - currentNormalTokenLays.clear(); - log.debug("This was a normal token lay"); - } + if (operatingCompany.get().isClosed()) continue; - if (currentNormalTokenLays.isEmpty()) { - log.debug("No more normal token lays are allowed"); - } else if (operatingCompany.get().getNumberOfFreeBaseTokens() == 0) { - log.debug("Normal token lay allowed by no more tokens"); - currentNormalTokenLays.clear(); - } else { - log.debug("A normal token lay is still allowed"); - } - setSpecialTokenLays(); - log.debug("There are now " + currentSpecialTokenLays.size() - + " special token lay objects"); - if (currentNormalTokenLays.isEmpty() - && currentSpecialTokenLays.isEmpty()) { - nextStep(); - } + return true; + } + } + protected void setOperatingCompany (PublicCompanyI company) { + if (operatingCompany == null) { + operatingCompany = + new GenericState<PublicCompanyI>("OperatingCompany", company); + } else { + operatingCompany.set(company); } + } - return true; + /** + * Get the public company that has the turn to operate. + * + * @return The currently operating company object. + */ + public PublicCompanyI getOperatingCompany() { + return operatingCompany.get(); } - public boolean layBonusToken(LayBonusToken action) { + public List<PublicCompanyI> getOperatingCompanies() { + return operatingCompanies.viewList(); + } - String errMsg = null; - int cost = 0; - SpecialTokenLay stl = null; - boolean extra = false; + public int getOperatingCompanyIndex() { + int index = operatingCompanies.indexOf(getOperatingCompany()); + return index; + } - MapHex hex = action.getChosenHex(); - BonusToken token = action.getToken(); - // Dummy loop to enable a quick jump out. - while (true) { + /*======================================= + * 2.4. STEP CONTROL + *=======================================*/ - // Checks - MapHex location = action.getChosenHex(); - if (location != hex) { - errMsg = - LocalText.getText("TokenLayingHexMismatch", - hex.getName(), - location.getName() ); - break; - } - stl = action.getSpecialProperty(); - if (stl != null) extra = stl.isExtra(); + /** + * Get the current operating round step (i.e. the next action). + * + * @return The number that defines the next action. + */ + public GameDef.OrStep getStep() { + return (GameDef.OrStep) stepObject.get(); + } - cost = 0; // Let's assume for now that bonus tokens are always - // free - if (stl != null && stl.isFree()) cost = 0; + /** + * Bypass normal order of operations and explicitly set round step. This + * should only be done for specific rails.game exceptions, such as forced + * train purchases. + * + * @param step + */ + protected void setStep(GameDef.OrStep step) { - // Does the company have the money? - if (cost > operatingCompany.get().getCash()) { - errMsg = - LocalText.getText("NotEnoughMoney", - operatingCompany.get().getName()); - break; - } - break; - } - if (errMsg != null) { - DisplayBuffer.add(LocalText.getText("CannotLayBonusTokenOn", - token.getName(), - hex.getName(), - Bank.format(cost), - errMsg )); - return false; - } + stepObject.set(step); - /* End of validation, start of execution */ - moveStack.start(true); + } - if (hex.layBonusToken(token, gameManager.getPhaseManager())) { - /* TODO: the false return value must be impossible. */ + /** + * Internal method: change the OR state to the next step. If the currently + * Operating Company is done, notify this. + * + * @param company The current company. + */ + protected void nextStep() { + nextStep(getStep()); + } - operatingCompany.get().addBonus(new Bonus(operatingCompany.get(), - token.getName(), - token.getValue(), Collections.singletonList(hex))); - token.setUser(operatingCompany.get()); + /** Take the next step after a given one (see nextStep()) */ + protected void nextStep(GameDef.OrStep step) { - ReportBuffer.add(LocalText.getText("LaysBonusTokenOn", - operatingCompany.get().getName(), - token.getName(), - Bank.format(token.getValue()), - hex.getName() )); + PublicCompanyI company = operatingCompany.get(); - // Was a special property used? - if (stl != null) { - stl.setExercised(); - currentSpecialTokenLays.remove(action); - log.debug("This was a special token lay, " - + (extra ? "" : " not") + " extra"); + // Cycle through the steps until we reach one where a user action is + // expected. + int stepIndex; + for (stepIndex = 0; stepIndex < steps.length; stepIndex++) { + if (steps[stepIndex] == step) break; + } + while (++stepIndex < steps.length) { + step = steps[stepIndex]; + log.debug("Step " + step); + if (step == GameDef.OrStep.LAY_TOKEN + && company.getNumberOfFreeBaseTokens() == 0) { + continue; } - } + if (step == GameDef.OrStep.CALC_REVENUE) { - return true; - } + if (!company.canRunTrains()) { + // No trains, then the revenue is zero. + executeSetRevenueAndDividend ( + new SetDividend (0, false, new int[] {SetDividend.NO_TRAIN})); + // TODO: This probably does not handle share selling correctly + continue; + } + } - public boolean buyBonusToken(BuyBonusToken action) { + if (step == GameDef.OrStep.PAYOUT) { + // This step is now obsolete + continue; + } - String errMsg = null; - int cost; - SellBonusToken sbt = null; - CashHolder seller = null; + if (step == GameDef.OrStep.TRADE_SHARES) { - // Dummy loop to enable a quick jump out. - while (true) { + // Is company allowed to trade trasury shares? + if (!company.mayTradeShares() + || !company.hasOperated()) { + continue; + } - // Checks - sbt = action.getSpecialProperty(); - cost = sbt.getPrice(); - seller = sbt.getSeller(); + /* Check if any trading is possible. + * If not, skip this step. + * (but register a Done action for BACKWARDS COMPATIBILITY only) + */ + // Preload some expensive results + int ownShare = company.getPortfolio().getShare(company); + int poolShare = pool.getShare(company); // Expensive, do it once + // Can it buy? + boolean canBuy = + ownShare < getGameParameterAsInt (GameDef.Parm.TREASURY_SHARE_LIMIT) + && company.getCash() >= company.getCurrentSpace().getPrice() + && poolShare > 0; + // Can it sell? + boolean canSell = + company.getPortfolio().getShare(company) > 0 + && poolShare < getGameParameterAsInt (GameDef.Parm.POOL_SHARE_LIMIT); + // Above we ignore the possible existence of double shares (as in 1835). + + if (!canBuy && !canSell) { + // XXX For BACKWARDS COMPATIBILITY only, + // register a Done skip action during reloading. + if (gameManager.isReloading()) { + gameManager.setSkipDone(GameDef.OrStep.TRADE_SHARES); + log.debug("If the next saved action is 'Done', skip it"); + } + log.info("Skipping Treasury share trading step"); + continue; + } + + gameManager.startTreasuryShareTradingRound(); - // Does the company have the money? - if (cost > operatingCompany.get().getCash()) { - errMsg = - LocalText.getText("NotEnoughMoney", - operatingCompany.get().getName(), - Bank.format(operatingCompany.get().getCash()), - Bank.format(cost)); - break; } + + if (!gameSpecificNextStep (step)) continue; + + // No reason found to skip this step break; } - if (errMsg != null) { - DisplayBuffer.add(LocalText.getText("CannotBuyBonusToken", - operatingCompany.get().getName(), - sbt.getName(), - seller.getName(), - Bank.format(cost), - errMsg )); - return false; + + if (step == GameDef.OrStep.FINAL) { + finishTurn(); + } else { + setStep(step); } - /* End of validation, start of execution */ - moveStack.start(true); + } - new CashMove (operatingCompany.get(), seller, cost); - operatingCompany.get().addBonus(new Bonus(operatingCompany.get(), - sbt.getName(), - sbt.getValue(), - sbt.getLocations())); + /** Stub, can be overridden in subclasses to check for extra steps */ + protected boolean gameSpecificNextStep (GameDef.OrStep step) { + return true; + } - ReportBuffer.add(LocalText.getText("BuysBonusTokenFrom", - operatingCompany.get().getName(), - sbt.getName(), - Bank.format(sbt.getValue()), - seller.getName(), - Bank.format(sbt.getPrice()))); + /** + * This method is only called at the start of each step (unlike + * updateStatus(), which is called after each user action) + */ + protected void prepareStep() { + GameDef.OrStep step = stepObject.value(); - sbt.setExercised(); + if (step == GameDef.OrStep.LAY_TRACK) { + // getNormalTileLays(); + } else if (step == GameDef.OrStep.LAY_TOKEN) { - return true; - } + } else { + currentSpecialProperties = null; + } + } + /*======================================= + * 3. COMMON ACTIONS (not bound to steps) + * 3.1. NOOPS + *=======================================*/ - public boolean setRevenueAndDividend(SetDividend action) { + public void skip() { + log.debug("Skip step " + stepObject.value()); + moveStack.start(true); + nextStep(); + } - String errMsg = validateSetRevenueAndDividend (action); + /** + * The current Company is done operating. + * + * @param company Name of the company that finished operating. + * @return False if an error is found. + */ + public boolean done() { - if (errMsg != null) { - DisplayBuffer.add(LocalText.getText( - "CannotProcessRevenue", - Bank.format (action.getActualRevenue()), - action.getCompanyName(), - errMsg - )); + if (operatingCompany.get().getPortfolio().getNumberOfTrains() == 0 + && operatingCompany.get().mustOwnATrain()) { + // FIXME: Need to check for valid route before throwing an + // error. + /* Check TEMPORARILY disabled + errMsg = + LocalText.getText("CompanyMustOwnATrain", + operatingCompany.getObject().getName()); + setStep(STEP_BUY_TRAIN); + DisplayBuffer.add(errMsg); return false; + */ } - moveStack.start(true); + moveStack.start(false); - ReportBuffer.add(LocalText.getText("CompanyRevenue", - action.getCompanyName(), - Bank.format(action.getActualRevenue()))); + nextStep(); - int remainingAmount = checkForDeductions (action); - if (remainingAmount < 0) { - // A share selling round will be run to raise cash to pay debts - return true; + if (getStep() == GameDef.OrStep.FINAL) { + finishTurn(); } - executeSetRevenueAndDividend (action); - return true; - } - protected String validateSetRevenueAndDividend (SetDividend action) { + /*======================================= + * 3.2. DISCARDING TRAINS + *=======================================*/ + + public boolean discardTrain(DiscardTrain action) { + + TrainI train = action.getDiscardedTrain(); + PublicCompanyI company = action.getCompany(); + String companyName = company.getName(); String errMsg = null; - PublicCompanyI company; - String companyName; - int amount = 0; - int revenueAllocation = -1; // Dummy loop to enable a quick jump out. while (true) { - // Checks - // Must be correct company. - company = action.getCompany(); - companyName = company.getName(); - if (company != operatingCompany.get()) { - errMsg = - LocalText.getText("WrongCompany", - companyName, - operatingCompany.get().getName() ); - break; - } // Must be correct step - if (getStep() != GameDef.OrStep.CALC_REVENUE) { - errMsg = LocalText.getText("WrongActionNoRevenue"); + if (getStep() != GameDef.OrStep.BUY_TRAIN + && getStep() != GameDef.OrStep.DISCARD_TRAINS) { + errMsg = LocalText.getText("WrongActionNoDiscardTrain"); break; } - // Amount must be non-negative multiple of 10 - amount = action.getActualRevenue(); - if (amount < 0) { - errMsg = - LocalText.getText("NegativeAmountNotAllowed", - String.valueOf(amount)); - break; - } - if (amount % 10 != 0) { - errMsg = - LocalText.getText("AmountMustBeMultipleOf10", - String.valueOf(amount)); + if (train == null && action.isForced()) { + errMsg = LocalText.getText("NoTrainSpecified"); break; } - // Check chosen revenue distribution - if (amount > 0) { - // Check the allocation type index (see SetDividend for values) - revenueAllocation = action.getRevenueAllocation(); - if (revenueAllocation < 0 - || revenueAllocation >= SetDividend.NUM_OPTIONS) { - errMsg = - LocalText.getText("InvalidAllocationTypeIndex", - String.valueOf(revenueAllocation)); - break; - } - - // Validate the chosen allocation type - int[] allowedAllocations = - ((SetDividend) selectedAction).getAllowedAllocations(); - boolean valid = false; - for (int aa : allowedAllocations) { - if (revenueAllocation == aa) { - valid = true; - break; - } - } - if (!valid) { - errMsg = - LocalText.getText(SetDividend.getAllocationNameKey(revenueAllocation)); - break; - } - } else { - // If there is no revenue, use withhold. - action.setRevenueAllocation(SetDividend.WITHHOLD); - } + // Does the company own such a train? - if (amount == 0 && operatingCompany.get().getNumberOfTrains() == 0) { - DisplayBuffer.add(LocalText.getText("RevenueWithNoTrains", - operatingCompany.get().getName(), - Bank.format(0) )); + if (!company.getPortfolio().getTrainList().contains(train)) { + errMsg = + LocalText.getText("CompanyDoesNotOwnTrain", + company.getName(), + train.getName() ); + break; } break; } + if (errMsg != null) { + DisplayBuffer.add(LocalText.getText("CannotDiscardTrain", + companyName, + (train != null ?train.getName() : "?"), + errMsg )); + return false; + } - return errMsg; - } + /* End of validation, start of execution */ + moveStack.start(true); + // + if (action.isForced()) moveStack.linkToPreviousMoveSet(); - protected void executeSetRevenueAndDividend (SetDividend action) { + // Reset type of dual trains + if (train.getCertType().getPotentialTrainTypes().size() > 1) { + train.setType(null); + } - int amount = action.getActualRevenue(); - int revenueAllocation = action.getRevenueAllocation(); + train.moveTo(train.isObsolete() ? scrapHeap : pool); + ReportBuffer.add(LocalText.getText("CompanyDiscardsTrain", + companyName, + train.getName() )); - operatingCompany.get().setLastRevenue(amount); - operatingCompany.get().setLastRevenueAllocation(revenueAllocation); + // Check if any more companies must discard trains, + // otherwise continue train buying + if (!checkForExcessTrains()) { + // Trains may have been discarded by other players + setCurrentPlayer (operatingCompany.get().getPresident()); + stepObject.set(GameDef.OrStep.BUY_TRAIN); + } - // Pay any debts from treasury, revenue and/or president's cash - // The remaining dividend may be less that the original income - amount = executeDeductions (action); + //setPossibleActions(); - if (amount == 0) { + return true; + } - ReportBuffer.add(LocalText.getText("CompanyDoesNotPayDividend", - operatingCompany.get().getName())); - withhold(amount); + protected void setTrainsToDiscard() { - } else if (revenueAllocation == SetDividend.PAYOUT) { + // Scan the players in SR sequence, starting with the current player + Player player; + List<PublicCompanyI> list; + int currentPlayerIndex = getCurrentPlayerIndex(); + for (int i = currentPlayerIndex; i < currentPlayerIndex + + getNumberOfPlayers(); i++) { + player = gameManager.getPlayerByIndex(i); + if (excessTrainCompanies.containsKey(player)) { + setCurrentPlayer(player); + list = excessTrainCompanies.get(player); + for (PublicCompanyI comp : list) { + possibleActions.add(new DiscardTrain(comp, + comp.getPortfolio().getUniqueTrains(), true)); + // We handle one company at at time. + // We come back here until all excess trains have been + // discarded. + return; + } + } + } + } + /*======================================= + * 3.3. PRIVATES (BUYING, SELLING, CLOSING) + *=======================================*/ - ReportBuffer.add(LocalText.getText("CompanyPaysOutFull", - operatingCompany.get().getName(), Bank.format(amount) )); + public boolean buyPrivate(BuyPrivate action) { - payout(amount); + String errMsg = null; + PublicCompanyI publicCompany = action.getCompany(); + String publicCompanyName = publicCompany.getName(); + PrivateCompanyI privateCompany = action.getPrivateCompany(); + String privateCompanyName = privateCompany.getName(); + int price = action.getPrice(); + CashHolder owner = null; + Player player = null; + int upperPrice; + int lowerPrice; - } else if (revenueAllocation == SetDividend.SPLIT) { + // Dummy loop to enable a quick jump out. + while (true) { - ReportBuffer.add(LocalText.getText("CompanySplits", - operatingCompany.get().getName(), Bank.format(amount) )); + // Checks + // Does private exist? + if ((privateCompany = + companyManager.getPrivateCompany( + privateCompanyName)) == null) { + errMsg = + LocalText.getText("PrivateDoesNotExist", + privateCompanyName); + break; + } + // Is private still open? + if (privateCompany.isClosed()) { + errMsg = + LocalText.getText("PrivateIsAlreadyClosed", + privateCompanyName); + break; + } + // Is private owned by a player? + owner = privateCompany.getPortfolio().getOwner(); + if (!(owner instanceof Player)) { + errMsg = + LocalText.getText("PrivateIsNotOwnedByAPlayer", + privateCompanyName); + break; + } + player = (Player) owner; + upperPrice = privateCompany.getUpperPrice(); + lowerPrice = privateCompany.getLowerPrice(); - splitRevenue(amount); + // Is private buying allowed? + if (!isPrivateSellingAllowed()) { + errMsg = LocalText.getText("PrivateBuyingIsNotAllowed"); + break; + } - } else if (revenueAllocation == SetDividend.WITHHOLD) { + // Price must be in the allowed range + if (lowerPrice != PrivateCompanyI.NO_PRICE_LIMIT && price < lowerPrice) { + errMsg = + LocalText.getText("PriceBelowLowerLimit", + Bank.format(price), + Bank.format(lowerPrice), + privateCompanyName ); + break; + } + if (upperPrice != PrivateCompanyI.NO_PRICE_LIMIT && price > upperPrice) { + errMsg = + LocalText.getText("PriceAboveUpperLimit", + Bank.format(price), + Bank.format(lowerPrice), + privateCompanyName ); + break; + } + // Does the company have the money? + if (price > operatingCompany.get().getCash()) { + errMsg = + LocalText.getText("NotEnoughMoney", + publicCompanyName, + Bank.format(operatingCompany.get().getCash()), + Bank.format(price) ); + break; + } + break; + } + if (errMsg != null) { + if (owner != null) { + DisplayBuffer.add(LocalText.getText("CannotBuyPrivateFromFor", + publicCompanyName, + privateCompanyName, + owner.getName(), + Bank.format(price), + errMsg )); + } else { + DisplayBuffer.add(LocalText.getText("CannotBuyPrivateFor", + publicCompanyName, + privateCompanyName, + Bank.format(price), + errMsg )); + } + return false; + } - ReportBuffer.add(LocalText.getText("CompanyWithholds", - operatingCompany.get().getName(), - Bank.format(amount) )); + moveStack.start(true); - withhold(amount); + operatingCompany.get().buyPrivate(privateCompany, player.getPortfolio(), + price); + + return true; + + } + + protected boolean isPrivateSellingAllowed() { + return getCurrentPhase().isPrivateSellingAllowed(); + } + protected int getPrivateMinimumPrice (PrivateCompanyI privComp) { + int minPrice = privComp.getLowerPrice(); + if (minPrice == PrivateCompanyI.NO_PRICE_LIMIT) { + minPrice = 0; } + return minPrice; + } - // Rust any obsolete trains - operatingCompany.get().getPortfolio().rustObsoleteTrains(); + protected int getPrivateMaximumPrice (PrivateCompanyI privComp) { + int maxPrice = privComp.getUpperPrice(); + if (maxPrice == PrivateCompanyI.NO_PRICE_LIMIT) { + maxPrice = operatingCompany.get().getCash(); + } + return maxPrice; + } - // We have done the payout step, so continue from there - nextStep(GameDef.OrStep.PAYOUT); + protected boolean maySellPrivate (Player player) { + return true; } - /** - * Distribute the dividend amongst the shareholders. - * - * @param amount - */ - public void payout(int amount) { + protected boolean executeClosePrivate(ClosePrivate action) { - if (amount == 0) return; + PrivateCompanyI priv = action.getPrivateCompany(); - int part; - int shares; + log.debug("Executed close private action for private " + priv.getName()); - Map<CashHolder, Integer> sharesPerRecipient = countSharesPerRecipient(); + String errMsg = null; - // Calculate, round up, report and add the cash + if (priv.isClosed()) + errMsg = LocalText.getText("PrivateAlreadyClosed", priv.getName()); - // Define a precise sequence for the reporting - Set<CashHolder> recipientSet = sharesPerRecipient.keySet(); - for (CashHolder recipient : SequenceUtil.sortCashHolders(recipientSet)) { - if (recipient instanceof Bank) continue; - shares = (sharesPerRecipient.get(recipient)); - if (shares == 0) continue; - part = (int) Math.ceil(amount * shares * operatingCompany.get().getShareUnit() / 100.0); - ReportBuffer.add(LocalText.getText("Payout", - recipient.getName(), - Bank.format(part), - shares, - operatingCompany.get().getShareUnit())); - pay (bank, recipient, part); + if (errMsg != null) { + DisplayBuffer.add(errMsg); + return false; } - // Move the token - operatingCompany.get().payout(amount); + moveStack.start(true); - } + priv.setClosed(); - protected Map<CashHolder, Integer> countSharesPerRecipient () { + return true; + } - Map<CashHolder, Integer> sharesPerRecipient = new HashMap<CashHolder, Integer>(); + /*======================================= + * 3.4. DESTINATIONS + *=======================================*/ - // Changed to accomodate the CGR 5% share roundup rule. - // For now it is assumed, that actual payouts are always rounded up - // (the withheld half of split revenues is not handled here, see splitRevenue()). + /** Stub for applying any follow-up actions when + * a company reaches it destinations. + * Default version: no actions. + * @param company + */ + protected void reachDestination (PublicCompanyI company) { - // First count the shares per recipient - for (PublicCertificateI cert : operatingCompany.get().getCertificates()) { - CashHolder recipient = getBeneficiary(cert); - if (!sharesPerRecipient.containsKey(recipient)) { - sharesPerRecipient.put(recipient, cert.getShares()); - } else { - sharesPerRecipient.put(recipient, - sharesPerRecipient.get(recipient) + cert.getShares()); - } - } - return sharesPerRecipient; } - /** Who gets the per-share revenue? */ - protected CashHolder getBeneficiary(PublicCertificateI cert) { + public boolean reachDestinations (ReachDestinations action) { - Portfolio holder = cert.getPortfolio(); - CashHolder beneficiary = holder.getOwner(); - // Special cases apply if the holder is the IPO or the Pool - if (operatingCompany.get().paysOutToTreasury(cert)) { - beneficiary = operatingCompany.get(); + List<PublicCompanyI> destinedCompanies + = action.getReachedCompanies(); + if (destinedCompanies != null) { + for (PublicCompanyI company : destinedCompanies) { + if (company.hasDestination() + && !company.hasReachedDestination()) { + if (!moveStack.isOpen()) moveStack.start(true); + company.setReachedDestination(true); + ReportBuffer.add(LocalText.getText("DestinationReached", + company.getName(), + company.getDestinationHex().getName() + )); + // Process any consequences of reaching a destination + // (default none) + reachDestination (company); + } + } } - return beneficiary; + return true; } /** - * Withhold a given amount of revenue (and store it). - * - * @param The revenue amount. + * This is currently a stub, as it is unclear if there is a common + * rule for setting destination reaching options. + * See OperatingRound_1856 for a first implementation + * of such rules. */ - public void withhold(int amount) { + protected void setDestinationActions () { - PublicCompanyI company = operatingCompany.get(); + } - // Payout revenue to company - pay (bank, company, amount); + /*======================================= + * ... [truncated message content] |