From a47ccfbd18c2c96b180901084d5dbfed3549925d Mon Sep 17 00:00:00 2001 From: erik-vos Date: Sun, 26 Mar 2023 23:46:28 +0200 Subject: [PATCH 1/3] 1826: Minimal change, to make installer checks rerun. --- src/main/java/net/sf/rails/game/Routes.java | 1 - src/main/resources/data/18VA/Map.xml | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/net/sf/rails/game/Routes.java b/src/main/java/net/sf/rails/game/Routes.java index fcfbaf85e..ec6812087 100644 --- a/src/main/java/net/sf/rails/game/Routes.java +++ b/src/main/java/net/sf/rails/game/Routes.java @@ -1,7 +1,6 @@ package net.sf.rails.game; import net.sf.rails.algorithms.*; -import org.jgrapht.graph.SimpleGraph; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/src/main/resources/data/18VA/Map.xml b/src/main/resources/data/18VA/Map.xml index 4acd5d640..16c9b3649 100644 --- a/src/main/resources/data/18VA/Map.xml +++ b/src/main/resources/data/18VA/Map.xml @@ -6,7 +6,7 @@ - + Date: Sun, 26 Mar 2023 23:46:28 +0200 Subject: [PATCH 2/3] 1826: Minimal change. --- src/main/java/net/sf/rails/game/Routes.java | 1 - src/main/resources/data/18VA/Map.xml | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/net/sf/rails/game/Routes.java b/src/main/java/net/sf/rails/game/Routes.java index fcfbaf85e..ec6812087 100644 --- a/src/main/java/net/sf/rails/game/Routes.java +++ b/src/main/java/net/sf/rails/game/Routes.java @@ -1,7 +1,6 @@ package net.sf.rails.game; import net.sf.rails.algorithms.*; -import org.jgrapht.graph.SimpleGraph; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/src/main/resources/data/18VA/Map.xml b/src/main/resources/data/18VA/Map.xml index 4acd5d640..16c9b3649 100644 --- a/src/main/resources/data/18VA/Map.xml +++ b/src/main/resources/data/18VA/Map.xml @@ -6,7 +6,7 @@ - + Date: Sun, 2 Apr 2023 14:58:09 +0200 Subject: [PATCH 3/3] 18VA: implementation near complete Added default revenue calculation methods to RevenueManager, and a subclass RevenueManager_18VA to provide real stop values for the initialization of each NetworkVertex, possibly depending on train type and company properties. So RevenueManager is no longer a final class. Also the predictionValue() method in dynamic modifiers is now obsolete. Also added a simple Revenue class, that carries two integers for normal and special revenue. For 18VA, special is used for the mine income that goes directly to the company treasury. The intention is to have revenue objects carry all revenues, but for now it is only applied to 18VA. --- .../sf/rails/algorithms/NetworkVertex.java | 50 +- .../sf/rails/algorithms/RevenueAdapter.java | 6 + .../sf/rails/algorithms/RevenueManager.java | 97 +- .../java/net/sf/rails/game/GameManager.java | 12 +- src/main/java/net/sf/rails/game/MapHex.java | 31 + .../net/sf/rails/game/OperatingRound.java | 60 +- .../net/sf/rails/game/PrivateCompany.java | 2 +- .../java/net/sf/rails/game/PublicCompany.java | 93 +- src/main/java/net/sf/rails/game/Revenue.java | 76 + src/main/java/net/sf/rails/game/Station.java | 11 +- src/main/java/net/sf/rails/game/Stop.java | 2 + src/main/java/net/sf/rails/game/Stops.java | 126 ++ .../sf/rails/game/model/BaseTokensModel.java | 2 +- .../sf/rails/game/model/PortfolioModel.java | 10 +- .../game/special/SpecialBaseTokenLay.java | 22 + .../sf/rails/game/special/SpecialRight.java | 5 +- .../rails/game/special/SpecialTrainBuy.java | 1 + .../specific/_1825/PublicCompany_1825.java | 2 +- .../specific/_1826/OperatingRound_1826.java | 2 +- .../specific/_1826/PublicCompany_1826.java | 46 +- .../specific/_1837/OperatingRound_1837.java | 3 +- .../specific/_1880/OperatingRound_1880.java | 2 +- .../specific/_1880/PublicCompany_1880.java | 2 +- .../_18EU/AlpineTokenRevenueModifier.java | 3 - .../game/specific/_18VA/GameDef_18VA.java | 17 + .../game/specific/_18VA/GameManager_18VA.java | 10 + .../specific/_18VA/OperatingRound_18VA.java | 102 + .../specific/_18VA/PublicCompany_18VA.java | 186 ++ .../specific/_18VA/RevenueManager_18VA.java | 119 ++ .../game/specific/_18VA/StartRound_18VA.java | 368 ++++ .../game/specific/_18VA/StockRound_18VA.java | 49 + .../rails/game/specific/_18VA/Stops_18VA.java | 65 + .../game/specific/_18VA/TrainRunModifier.java | 126 +- .../java/net/sf/rails/game/state/Ownable.java | 2 +- .../sf/rails/tools/ListAndFixSavedFiles.java | 49 +- .../net/sf/rails/ui/swing/hexmap/GUIHex.java | 29 +- .../java/rails/game/action/BidStartItem.java | 8 + .../java/rails/game/action/DiscardTrain.java | 2 +- src/main/resources/LocalisedText.properties | 1 + .../resources/data/18VA/CompanyManager.xml | 50 +- src/main/resources/data/18VA/Game.xml | 16 +- src/main/resources/data/18VA/Map.xml | 4 +- src/main/resources/data/18VA/TileSet.xml | 14 +- src/main/resources/data/GamesList.xml | 29 +- src/test/resources/data/real/18EUK41.rails | Bin 0 -> 41157 bytes src/test/resources/data/real/18EUK41.report | 1836 +++++++++++++++++ src/test/resources/data/real/18EUR42.rails | Bin 0 -> 40218 bytes src/test/resources/data/real/18EUR42.report | 1781 ++++++++++++++++ src/test/resources/data/real/18EUU43.rails | Bin 0 -> 41102 bytes src/test/resources/data/real/18EUU43.report | 1790 ++++++++++++++++ src/test/resources/data/real/18VAZ22.rails | Bin 0 -> 28610 bytes src/test/resources/data/real/18VAZ22.report | 1355 ++++++++++++ 52 files changed, 8415 insertions(+), 259 deletions(-) create mode 100644 src/main/java/net/sf/rails/game/Revenue.java create mode 100644 src/main/java/net/sf/rails/game/Stops.java create mode 100644 src/main/java/net/sf/rails/game/specific/_18VA/OperatingRound_18VA.java create mode 100644 src/main/java/net/sf/rails/game/specific/_18VA/PublicCompany_18VA.java create mode 100644 src/main/java/net/sf/rails/game/specific/_18VA/RevenueManager_18VA.java create mode 100644 src/main/java/net/sf/rails/game/specific/_18VA/StartRound_18VA.java create mode 100644 src/main/java/net/sf/rails/game/specific/_18VA/StockRound_18VA.java create mode 100644 src/main/java/net/sf/rails/game/specific/_18VA/Stops_18VA.java create mode 100644 src/test/resources/data/real/18EUK41.rails create mode 100644 src/test/resources/data/real/18EUK41.report create mode 100644 src/test/resources/data/real/18EUR42.rails create mode 100644 src/test/resources/data/real/18EUR42.report create mode 100644 src/test/resources/data/real/18EUU43.rails create mode 100644 src/test/resources/data/real/18EUU43.report create mode 100644 src/test/resources/data/real/18VAZ22.rails create mode 100644 src/test/resources/data/real/18VAZ22.report diff --git a/src/main/java/net/sf/rails/algorithms/NetworkVertex.java b/src/main/java/net/sf/rails/algorithms/NetworkVertex.java index 8c871d00f..e3be59525 100644 --- a/src/main/java/net/sf/rails/algorithms/NetworkVertex.java +++ b/src/main/java/net/sf/rails/algorithms/NetworkVertex.java @@ -49,6 +49,7 @@ public enum StationType { // only for station objects private final Stop stop; + private RevenueManager revenueManager; /** constructor for station on mapHex */ public NetworkVertex(MapHex hex, Station station) { @@ -64,6 +65,8 @@ public NetworkVertex(MapHex hex, Station station) { this.virtual = false; this.virtualId = null; + + this.revenueManager = hex.getRoot().getRevenueManager(); } /** constructor for side on mapHex */ @@ -157,6 +160,7 @@ public int getValue() { return value; } + /* public int getValueByTrain(NetworkTrain train) { int valueByTrain; if (isMajor()) { @@ -174,6 +178,41 @@ public int getValueByTrain(NetworkTrain train) { valueByTrain = value; } return valueByTrain; + }*/ + public int getValueByTrain (NetworkTrain train) { + /* + int valueByTrain = 0; + switch (stop.getType()) { + case CITY: + valueByTrain = value * train.getMultiplyMajors(); + break; + case TOWN: + if (!train.ignoresMinors()) { + valueByTrain = value * train.getMultiplyMinors(); + } + break; + case OFFMAP: + valueByTrain = hex.getCurrentValueForPhase( + hex.getRoot().getPhaseManager().getCurrentPhase()); + break; + case MINE: + // For 18VA (see GameManager_18VA). Default return value is 0. + valueByTrain = hex.getValuePerTrain(train.getRailsTrain()); + break; + case PASS: + valueByTrain = 0; + break; + default: + valueByTrain = value; + break; + } + return valueByTrain;*/ + Train railsTrain= train.getRailsTrain(); + PublicCompany company = (PublicCompany)railsTrain.getOwner(); + int revenue = revenueManager.getActualAsInteger(stop, railsTrain, + company); + log.debug("+++++ Vertex {} has value {} for train {} of {}", this, revenue, train, company); + return revenue; } public NetworkVertex setValue(int value) { @@ -244,7 +283,7 @@ public boolean initRailsVertex(PublicCompany company, boolean running) { // if company == null, then no vertex gets removed if (company != null && !stop.isRunToAllowedFor(company, running) && !stop.isRunThroughAllowedFor(company)) { - log.info("Vertex is removed"); + log.debug("Vertex is removed"); return false; } @@ -252,7 +291,11 @@ public boolean initRailsVertex(PublicCompany company, boolean running) { if (stop.getScoreType() == Access.Score.MAJOR) { setStationType(StationType.MAJOR); } else if (stop.getScoreType() == Access.Score.MINOR) { - setStationType(StationType.MINOR); + //if (stop.getAccess() != null && stop.getAccess().getType() == Stop.Type.MINE) { + // setStationType(StationType.COALMINE); + //} else { + setStationType(StationType.MINOR); + //} } else if (stop.getScoreType() == Access.Score.NO) { // Used in 18EU Alpine variant setStationType(StationType.PASS); // Not sure if this is sensible for 18EU } @@ -285,8 +328,6 @@ public boolean initRailsVertex(PublicCompany company, boolean running) { public void setRailsVertexValue(Phase phase) { // side vertices and virtuals cannot use this function if (virtual || type == VertexType.SIDE) return; - - // define value value = stop.getValueForPhase(phase); } @@ -524,6 +565,7 @@ public static Rectangle getVertexMapCoverage(HexMap map, Collection getOptimalRun() { if (optimalRun == null) { optimalRun = convertRcRun(rc.getOptimalRun()); diff --git a/src/main/java/net/sf/rails/algorithms/RevenueManager.java b/src/main/java/net/sf/rails/algorithms/RevenueManager.java index f05b63c62..d7e45f5b3 100644 --- a/src/main/java/net/sf/rails/algorithms/RevenueManager.java +++ b/src/main/java/net/sf/rails/algorithms/RevenueManager.java @@ -8,9 +8,7 @@ import net.sf.rails.common.parser.Configurable; import net.sf.rails.common.parser.ConfigurationException; import net.sf.rails.common.parser.Tag; -import net.sf.rails.game.PublicCompany; -import net.sf.rails.game.RailsManager; -import net.sf.rails.game.RailsRoot; +import net.sf.rails.game.*; import net.sf.rails.game.state.ArrayListState; import org.slf4j.Logger; @@ -18,14 +16,16 @@ /** - * Coordinates and stores all elements related to revenue calulcation, + * Coordinates and stores all elements related to revenue calculation, * which are permanent. * The conversion of Rails elements is in the responsibility of the RevenueAdapter. * For each GameManager instance only one RevenueManager is created. */ -public final class RevenueManager extends RailsManager implements Configurable { +public class RevenueManager extends RailsManager implements Configurable { - private int specialRevenue; + protected int specialRevenue; + protected RailsRoot root; + protected PhaseManager phaseManager; private static final Logger log = LoggerFactory.getLogger(RevenueManager.class); @@ -49,6 +49,8 @@ public final class RevenueManager extends RailsManager implements Configurable { */ public RevenueManager(RailsRoot parent, String id) { super(parent, id); + this.root = parent; + this.phaseManager = root.getPhaseManager(); } public void configureFromXML(Tag tag) throws ConfigurationException { @@ -206,7 +208,7 @@ boolean initDynamicModifiers(RevenueAdapter revenueAdapter) { * @return revenue from active calculator */ // FIXME: This does not fully cover all cases that needs the revenue from the calculator - // EV: indeed, it used in a different way in 1837, so beware! + // EV: indeed, it is used in a different way in 1837, so beware! // See RunToCoalMineModifier. int revenueFromDynamicCalculator(RevenueAdapter revenueAdapter) { return calculatorModifier.calculateRevenue(revenueAdapter); @@ -271,7 +273,7 @@ int predictionValue(List run) { * @param revenueAdapter * @return pretty print output from all modifiers (both static and dynamic) */ - String prettyPrint(RevenueAdapter revenueAdapter) { + protected String prettyPrint(RevenueAdapter revenueAdapter) { StringBuilder prettyPrint = new StringBuilder(); for (RevenueStaticModifier modifier : activeStaticModifiers) { @@ -291,5 +293,84 @@ String prettyPrint(RevenueAdapter revenueAdapter) { return prettyPrint.toString(); } + /*--------------------------- + * The below new section of RevenueManager provides a generic interface + * to obtain the actual revenue values of stops of any kind. + * Game-specific subclasses can provide special cases. + * + * These methods are used by + * - NetworkVertex, to initialise stop values per train + * in getValueByTrain(). + * - Dynamic modifiers (currently 18VA only, TBD). + * + * The methods getBaseRevenue() and getExtraRevenue() can be + * overridden in subclasses of RevenueManager (which is no longer final). + * This allows to specify the actual revenue value of stops + * per stop type, train type en company details, as needed. + * + * Method getExtraRevenue() was intended to return any extra values + * that should be returned by predictionValue() in dynamic modifiers. + * However, the flexibility of getBaseRevenue() should allow + * getExtraRevenue to return zero at all times. + * + * Revenues are returned as objects of the new Revenue class, + * which can carry both normal and special revenue values. + * Special values include the direct-to-treasury amounts + * of 1837 and 18VA (revenue from mines). + * + * Created 04/2023 by Erik Vos + */ + + /* 'Actual revenue' is the final value of a stop for a given + * train and company. This is the value with which NetworkVertex + * objects can b e initialized. + * + * @param stop The stop for which a revenue value is requested + * @param train A specific train that may affect the revenue, + * or null if the train does not matter + * @param company A specific company that may affect the revenue, + * or null if the company does not matter + * @return A new Revenue object + */ + public final Revenue getActualRevenue(Stop stop, Train train, PublicCompany company) { + return getBaseRevenue (stop, train, company) + .addRevenue (getExtraRevenue(stop, train, company)); + } + + public final int getActualAsInteger (Stop stop, Train train, PublicCompany company) { + Revenue rev = getActualRevenue(stop, train, company); + return rev.getNormalRevenue() + rev.getSpecialRevenue(); + } + + /** Same as getRevenue(), but intended to get + * additional revenue vales, as are needed for + * the predictionValue() methods in dynamic modifiers. + * + * This method may not be really needed anymore, as getActualRevenue + * now provides the final value per NetworkVertex, rather than a loosely + * predicted one. + */ + public Revenue getExtraRevenue (Stop stop, Train train, PublicCompany company) { + + return new Revenue (0, 0); + } + + public final int getExtraAsInteger (Stop stop, Train train, PublicCompany company) { + Revenue rev = getExtraRevenue(stop, train, company); + return rev.getNormalRevenue() + rev.getSpecialRevenue(); + } + + /** Return the revenue insofar it could be configured. + * This probably represents the former initial NetworkVertex revenue, + * and it still is the default for the new actual revenue. + * + * @param stop + * @param train + * @param company + * @return + */ + protected Revenue getBaseRevenue (Stop stop, Train train, PublicCompany company) { + return new Revenue (stop.getValueForPhase(phaseManager.getCurrentPhase()),0); + } } diff --git a/src/main/java/net/sf/rails/game/GameManager.java b/src/main/java/net/sf/rails/game/GameManager.java index 485ff8703..9c574d1f5 100644 --- a/src/main/java/net/sf/rails/game/GameManager.java +++ b/src/main/java/net/sf/rails/game/GameManager.java @@ -594,7 +594,7 @@ public void nextRound(Round round) { if (currentPhase.getNumberOfOperatingRounds() != numOfORs.value()) { numOfORs.set(currentPhase.getNumberOfOperatingRounds()); } - log.info("Phase={} ORs={}", currentPhase.toText(), numOfORs); + log.debug("Phase={} ORs={}", currentPhase.toText(), numOfORs); // Create a new OperatingRound (never more than one Stock Round) // OperatingRound.resetRelativeORNumber(); @@ -1935,6 +1935,16 @@ public boolean isTrainBlocked (Train train) { return blockedTrains.contains(train); } + /** Stub for train-type dependent stop values. + * Used in 18VA for CMD value (see GameManager_18VA). + * The given default value of 0 is correct for 1837. + * @param train The train + * @return The train-dependent value of a stop + */ + public int getValuePerTrain (Train train) { + return 0; + } + //------------------------------------ // Random generator diff --git a/src/main/java/net/sf/rails/game/MapHex.java b/src/main/java/net/sf/rails/game/MapHex.java index 0f39d813c..98b74cd03 100644 --- a/src/main/java/net/sf/rails/game/MapHex.java +++ b/src/main/java/net/sf/rails/game/MapHex.java @@ -35,6 +35,12 @@ */ public class MapHex extends RailsModel implements RailsOwner, Configurable { + public enum ValueType { + PERTILE, // Default + PERPHASE, // Default for offmap tiles + PERTRAIN // So far for 18VA CMD only + } + private static final Logger log = LoggerFactory.getLogger(MapHex.class); public static class Coordinates { @@ -166,6 +172,7 @@ public String toString() { * Values if this is an off-board hex */ private List valuesPerPhase = null; + private ValueType valueType = ValueType.PERTILE; /* * Temporary storage for impassable hexsides. Once neighbours has been set @@ -254,6 +261,9 @@ public enum BlockedToken { private final PortfolioSet bonusTokens = PortfolioSet.create(this, "bonusTokens", BonusToken.class); + private final PortfolioSet offStationBaseTokens + = PortfolioSet.create(this, "offStationBaseTokens", BaseToken.class); + /** * Parameters for extra text to be printed at a specified position on the hex. * Added for 1837 to print coal mine names @@ -295,7 +305,10 @@ public void configureFromXML(Tag tag) throws ConfigurationException { label = tag.getAttributeAsString("label", ""); // Off-board revenue values + String valueTypeString = tag.getAttributeAsString("valueType", "perTile"); + valueType = ValueType.valueOf(valueTypeString.toUpperCase()); valuesPerPhase = tag.getAttributeAsIntegerList("value"); + if (!valuesPerPhase.isEmpty()) valueType = ValueType.PERPHASE; // Location name stopName = tag.getAttributeAsString("city", ""); @@ -829,6 +842,12 @@ public boolean layBaseToken(PublicCompany company, Stop stop) { } } + /** Lay an off-station base token, as in 18VA */ + public boolean layOffStationBaseToken (BaseToken token) { + offStationBaseTokens.add(token); + return true; + } + /** * Lay a bonus token. * @@ -856,6 +875,10 @@ public PortfolioSet getBonusTokens() { return bonusTokens; } + public PortfolioSet getOffStationBaseTokens() { + return offStationBaseTokens; + } + public boolean hasTokenSlotsLeft(Station station) { // FIXME: Is this still required // if (station == 0) station = 1; // Temp. fix for old save files @@ -1099,6 +1122,10 @@ public List getValuesPerPhase() { return valuesPerPhase; } + public ValueType getValueType() { + return valueType; + } + public int getCurrentValueForPhase(Phase phase) { if (hasValuesPerPhase() && phase != null) { return valuesPerPhase.get(Math.min(valuesPerPhase.size(), @@ -1108,6 +1135,10 @@ public int getCurrentValueForPhase(Phase phase) { } } + public int getValuePerTrain (Train train) { + return getRoot().getGameManager().getValuePerTrain(train); + } + public String getStopName() { return stopName; } diff --git a/src/main/java/net/sf/rails/game/OperatingRound.java b/src/main/java/net/sf/rails/game/OperatingRound.java index a6f005dab..2175ba75e 100644 --- a/src/main/java/net/sf/rails/game/OperatingRound.java +++ b/src/main/java/net/sf/rails/game/OperatingRound.java @@ -11,7 +11,6 @@ import net.sf.rails.game.state.Observer; import net.sf.rails.game.state.*; import net.sf.rails.util.SequenceUtil; -import net.sf.rails.util.Util; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import rails.game.action.*; @@ -910,10 +909,14 @@ protected void nextStep(GameDef.OrStep step) { log.debug("OR considers newStep {}", newStep); if (newStep == GameDef.OrStep.LAY_TRACK) { + if (noMapMode) continue; initNormalTileLays(); } if (newStep == GameDef.OrStep.LAY_TOKEN) { + + if (noMapMode) continue; + log.debug ("No normal token laid yet"); /* List bonuses = gameManager.getCommonSpecialProperties(); boolean bonusTokensForSale = @@ -2152,6 +2155,8 @@ public boolean layBaseToken(LayBaseToken action) { PublicCompany company = action.getCompany(); String companyName = company.getId(); + boolean nonCity = (action.getType() == LayBaseToken.NON_CITY); + // Dummy loop to enable a quick jump out. while (true) { @@ -2162,7 +2167,8 @@ public boolean layBaseToken(LayBaseToken action) { && action.getType() != LayBaseToken.HOME_CITY && action.getType() != LayBaseToken.SPECIAL_PROPERTY && action.getType() != LayBaseToken.CORRECTION - && action.getType() != LayBaseToken.FORCED_LAY) { + && action.getType() != LayBaseToken.FORCED_LAY + && !nonCity) { errMsg = LocalText.getText("WrongActionNoTokenLay"); break; } @@ -2172,12 +2178,12 @@ public boolean layBaseToken(LayBaseToken action) { break; } - if (!isTokenLayAllowed(company, hex, stop)) { + if (!nonCity && !isTokenLayAllowed(company, hex, stop)) { errMsg = LocalText.getText("BaseTokenSlotIsReserved"); break; } - if (!stop.hasTokenSlotsLeft()) { + if (!nonCity && !stop.hasTokenSlotsLeft()) { errMsg = LocalText.getText("CityHasNoEmptySlots"); break; } @@ -2238,8 +2244,14 @@ public boolean layBaseToken(LayBaseToken action) { } /* End of validation, start of execution */ + boolean result; - if (hex.layBaseToken(company, stop)) { + if (nonCity) { + result = hex.layOffStationBaseToken(BaseToken.create(company)); + } else { + result = hex.layBaseToken(company, stop); + } + if (result) { /* TODO: the false return value must be impossible. */ company.layBaseToken(hex, cost); @@ -2286,7 +2298,7 @@ public boolean layBaseToken(LayBaseToken action) { if (currentNormalTokenLays.isEmpty()) { log.debug("No more normal token lays are allowed"); } else if (operatingCompany.value().getNumberOfFreeBaseTokens() == 0) { - log.debug("Normal token lay allowed by no more tokens"); + log.debug("Normal token lay allowed but no more tokens"); currentNormalTokenLays.clear(); } else { log.debug("A normal token lay is still allowed"); @@ -2295,7 +2307,8 @@ public boolean layBaseToken(LayBaseToken action) { log.debug("There are now {} special token lay objects", currentSpecialTokenLays.size()); // Can more tokens be laid? Otherwise, next step - if (!canLayAnyTokens(false)) { + //if (!canLayAnyTokens(false)) { + if (currentNormalTokenLays.isEmpty() && currentSpecialTokenLays.isEmpty()) { nextStep(); } @@ -2550,11 +2563,16 @@ protected void setBonusTokenLays() { */ protected boolean canLayAnyTokens (boolean resetTokenLays) { + if (resetTokenLays) setNormalTokenLays(); if (!currentNormalTokenLays.isEmpty()) return true; if (resetTokenLays) setSpecialTokenLays(); if (!currentSpecialTokenLays.isEmpty()) return true; if (!getSpecialProperties(SpecialBonusTokenLay.class).isEmpty()) return true; + for (SpecialBaseTokenLay sbtl : getSpecialProperties(SpecialBaseTokenLay.class)) { + if (operatingCompany.value().getNumberOfFreeBaseTokens() > 0 + || sbtl.isCreate()) return true; + } return false; } @@ -2790,13 +2808,26 @@ protected void executeSetRevenueAndDividend(SetDividend action, String report) { /** * Process any special revenue, adapting the dividend as required. * Default version: dividend = earnings. - * To be overridden if any special revenue must be processed. + * Should not be called if specialRevenue = 0. * @param earnings The total income from train runs. * @param specialRevenue Any income that needs special processing. * @return The resulting dividend (default: equal to the earnings). */ protected int processSpecialRevenue(int earnings, int specialRevenue) { - return earnings; + int dividend = earnings; + PublicCompany company = operatingCompany.value(); + if (specialRevenue > 0) { + dividend -= specialRevenue; + company.setLastDirectIncome(specialRevenue); + ReportBuffer.add(this, LocalText.getText("CompanyDividesEarnings", + company, + Bank.format(this, earnings), + Bank.format(this, dividend), + Bank.format(this, specialRevenue))); + Currency.fromBank(specialRevenue, company); + } + company.setLastDividend(dividend); + return dividend; } /* @@ -2838,7 +2869,7 @@ public void payout(int amount) { } // Move the token - operatingCompany.value().payout(amount); + operatingCompany.value().adjustPriceOnPayout(amount); } protected Map countSharesPerRecipient() { @@ -2871,14 +2902,17 @@ protected MoneyOwner getBeneficiary(PublicCertificate cert) { MoneyOwner beneficiary; // Special cases apply if the holder is the IPO or the Pool + beneficiary = bank; // Default if (operatingCompany.value().paysOutToTreasury(cert)) { beneficiary = operatingCompany.value(); + } else if (cert.getOwner().equals(operatingCompany.value())) { + if (operatingCompany.value().treasurySharesPayOut(cert)) { + beneficiary = operatingCompany.value(); + } } else if (cert.getOwner() instanceof MoneyOwner) { beneficiary = (MoneyOwner) cert.getOwner(); - } else { // TODO: check if this is a correct assumption that otherwise - // the money goes to the bank - beneficiary = bank; } + return beneficiary; } diff --git a/src/main/java/net/sf/rails/game/PrivateCompany.java b/src/main/java/net/sf/rails/game/PrivateCompany.java index f7d5e0565..f8b9f4a0c 100644 --- a/src/main/java/net/sf/rails/game/PrivateCompany.java +++ b/src/main/java/net/sf/rails/game/PrivateCompany.java @@ -174,7 +174,7 @@ public void configureFromXML(Tag tag) throws ConfigurationException { String ifAttribute = spTag.getAttributeAsString("condition"); if (ifAttribute != null) { closeIfAllExercised = "ifExercised".equalsIgnoreCase(ifAttribute) - || ifAttribute.equalsIgnoreCase("ifAllExercised"); + || "ifAllExercised".equalsIgnoreCase(ifAttribute); closeIfAnyExercised = "ifAnyExercised".equalsIgnoreCase(ifAttribute); } String whenAttribute = spTag.getAttributeAsString("when"); diff --git a/src/main/java/net/sf/rails/game/PublicCompany.java b/src/main/java/net/sf/rails/game/PublicCompany.java index 6a92d55a1..20e41fe71 100644 --- a/src/main/java/net/sf/rails/game/PublicCompany.java +++ b/src/main/java/net/sf/rails/game/PublicCompany.java @@ -12,6 +12,7 @@ import net.sf.rails.game.state.Currency; import net.sf.rails.game.state.Observable; import net.sf.rails.game.state.*; +import net.sf.rails.game.state.Observer; import net.sf.rails.util.Util; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -139,7 +140,7 @@ public static BaseCostMethod get(String configName) { */ protected int publicNumber = -1; // For internal use - protected int numberOfBaseTokens = 0; + protected IntegerState numberOfBaseTokens = IntegerState.create(this, "noOfBaseTokens"); /** * In case of distance-dependent token lay cost, @@ -296,9 +297,7 @@ public static BaseCostMethod get(String configName) { protected boolean poolPaysOut = false; - /* not used - protected boolean treasuryPaysOut = false; - */ + protected boolean treasuryPaysOut = true; // Used by 18VA: false protected boolean canHoldOwnShares = false; @@ -560,8 +559,6 @@ public PublicCompany(RailsItem parent, String id, boolean hasStockPrice) { currentPrice = PriceModel.create(this, "currentPrice", true); canSharePriceVary = new BooleanState(this, "canSharePriceVary", true); } - - } /** @@ -597,7 +594,7 @@ public void configureFromXML(Tag tag) throws ConfigurationException { fixedPrice = tag.getAttributeAsInteger("price", 0); - numberOfBaseTokens = tag.getAttributeAsInteger("tokens", 1); + numberOfBaseTokens.set(tag.getAttributeAsInteger("tokens", 1)); certsAreInitiallyAvailable = tag.getAttributeAsBoolean("available", certsAreInitiallyAvailable); @@ -646,8 +643,9 @@ public void configureFromXML(Tag tag) throws ConfigurationException { parentInfoText += SpecialProperty.configure(this, tag); poolPaysOut = poolPaysOut || tag.getChild("PoolPaysOut") != null; - ipoPaysOut = ipoPaysOut || tag.getChild("IPOPaysOut") != null; + treasuryPaysOut = treasuryPaysOut + && tag.getChild("TreasuryDoesNotPayOut") == null; Tag floatTag = tag.getChild("Float"); if (floatTag != null) { @@ -1167,15 +1165,57 @@ public void setReachedDestination(boolean value) { hasReachedDestination.set(value); } - /** Stub to trigger a company to make more shares available, - * in other words: become a higher-number-of-shares company. - * @return false if conversion is not allowed or fails. - * - * Used by overriding in 1826 (perhaps that code could be put here) - */ - public boolean grow() { - return validateGrow(); + /** Convert company from a 5-share to a 10-share company */ + /* The intention is to make this code usable for other games as well. */ + public boolean grow () { + + if (!validateGrow()) return false; + + growStep.add(1); + setShareUnit(shareUnitSizes.get(growStep.value())); + + BankPortfolio reserved = getRoot().getBank().getUnavailable(); + BankPortfolio ipo = getRoot().getBank().getIpo(); + Set last5Shares = reserved.getPortfolioModel().getCertificates(this); + for (PublicCertificate cert : last5Shares) { + if (hasStarted()) { + cert.moveTo(this); + } else { + // Still in IPO, put the reserved shares there too + cert.moveTo(ipo); + } + } + + ReportBuffer.add(this, LocalText.getText("CompanyHasGrown", + this, getActiveShareCount())); + + currentTrainLimits.setTo(trainLimits.get(growStep.value())); + ReportBuffer.add(this, + LocalText.getText("PhaseDependentTrainLimitsSetTo", + this, currentTrainLimits.view(), getCurrentTrainLimit())); + + + // For some reason the shareUnit change does not update + // the percentages shown in the GameStatus window. + // E.g. 60% should become 30%, etc. + // There must be a nicer way to accomplish that, + // but for now the below code works. + Set modelsToUpdate = new HashSet<>(); + PortfolioOwner owner; + Model model; + for (PublicCertificate cert : getCertificates()) { + owner = (PortfolioOwner) cert.getOwner(); + model = owner.getPortfolioModel().getShareModel(this); + if (!modelsToUpdate.contains(model)) modelsToUpdate.add(model); + } + for (Model m : modelsToUpdate) { + for (Observer obs : m.getObservers()) { + obs.update(m.toText()); + } + } + return true; } + /** Stub, to be extended or overridden by specific games if needed. */ protected boolean validateGrow() { return growStep.value() < shareUnitSizes.size() - 1; @@ -1637,11 +1677,9 @@ public StringState getLastRevenueAllocationModel() { * Determine if the price token must be moved after a dividend payout, * and with how many jumps. * - * TODO: Will be renamed to adjustPriceOnPayout - * * @param amount The total revenue that has been paid out */ - public void payout(int amount) { + public void adjustPriceOnPayout(int amount) { if (!hasStockPrice || amount == 0) return; @@ -1671,6 +1709,11 @@ public void payout(int amount) { */ } + /** Do IPO or Pool pay out to the company? + * Default = false + * @param cert The certificate + * @return whether it pays out to the company if opwned by the Bank + */ public boolean paysOutToTreasury(PublicCertificate cert) { Owner owner = cert.getOwner(); @@ -1678,6 +1721,16 @@ public boolean paysOutToTreasury(PublicCertificate cert) { || owner == getRoot().getBank().getPool() && poolPaysOut; } + /** Do treasury shares pay out to the company? + * Default = true + * @param cert The certificate + * @return whether it pays out to its owning company + */ + public boolean treasurySharesPayOut (PublicCertificate cert) { + Owner owner = cert.getOwner(); + return owner == cert.getCompany() && treasuryPaysOut; + } + /** * Determine if the price token must be moved after a withheld dividend. * @@ -2253,7 +2306,7 @@ public boolean hasLaidHomeBaseTokens() { */ protected void initBaseTokens() { SortedSet newTokens = new TreeSet<>(); - for (int i = 0; i < numberOfBaseTokens; i++) { + for (int i = 0; i < numberOfBaseTokens.value(); i++) { BaseToken token = BaseToken.create(this); newTokens.add(token); } diff --git a/src/main/java/net/sf/rails/game/Revenue.java b/src/main/java/net/sf/rails/game/Revenue.java new file mode 100644 index 000000000..78864f607 --- /dev/null +++ b/src/main/java/net/sf/rails/game/Revenue.java @@ -0,0 +1,76 @@ +package net.sf.rails.game; + +/** Class Revenue is a wrapper around different revenue values. + * Specifically, it is intended for the 'direct' revenue amounts + * of 1837 and 18VA that go directly into the company treasury + * rather than being paid out as dividends. + * + * Currently, this class is not yet being applied in the whole + * algorithms tombola, but only by the other new class Stops + * and the 18VA TrainRunModifier. + * + * Created 04/2023 by Erik Vos + */ +public class Revenue { + + private int normalRevenue = 0; + private int specialRevenue = 0; + + public Revenue(int normalRevenue, int specialRevenue) { + this.normalRevenue = normalRevenue; + this.specialRevenue = specialRevenue; + } + + public Revenue (int normalRevenue) { + this.normalRevenue = normalRevenue; + } + + public int getNormalRevenue() { + return normalRevenue; + } + + public void setNormalRevenue(int normalRevenue) { + this.normalRevenue = normalRevenue; + } + + public int getSpecialRevenue() { + return specialRevenue; + } + + public void setSpecialRevenue(int specialRevenue) { + this.specialRevenue = specialRevenue; + } + + public Revenue addNormalRevenue (int normalRevenue) { + this.normalRevenue += normalRevenue; + return this; + } + + public Revenue addSpecialRevenue (int specialRevenue) { + this.specialRevenue += specialRevenue; + return this; + } + + public Revenue addRevenue (int normalRevenue, int specialRevenue) { + this.normalRevenue += normalRevenue; + this.specialRevenue += specialRevenue; + return this; + } + + public Revenue addRevenue (Revenue revenue) { + this.normalRevenue += revenue.normalRevenue; + this.specialRevenue += revenue.specialRevenue; + return this; + } + + public Revenue multiplyRevenue (int factor) { + normalRevenue *= factor; + specialRevenue *= factor; + return this; + } + + public String toString() { + return "{" + normalRevenue + "," + specialRevenue + "}"; + } + +} diff --git a/src/main/java/net/sf/rails/game/Station.java b/src/main/java/net/sf/rails/game/Station.java index 9acedd831..1b16a9afb 100644 --- a/src/main/java/net/sf/rails/game/Station.java +++ b/src/main/java/net/sf/rails/game/Station.java @@ -4,6 +4,7 @@ import net.sf.rails.common.parser.ConfigurationException; import net.sf.rails.common.parser.Tag; +import net.sf.rails.game.state.IntegerState; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -33,7 +34,7 @@ public class Station extends TrackPoint implements Comparable { private final Stop.Type type; private final int number; private final int value; - private final int baseSlots; + private IntegerState baseSlots; private final Tile tile; private final int position; private final String stopName; @@ -52,7 +53,7 @@ private Station(Tile tile, int number, String id, Stop.Type type, int value, this.id = id; this.type = type; this.value = value; - this.baseSlots = slots; + this.baseSlots = IntegerState.create (tile, "BTSlots_"+tile.getId()+"_"+number, slots); this.position = position; this.stopName = cityName; @@ -106,6 +107,10 @@ public static Station create(Tile tile, Tag stationDefTag, Tag stationSetTag) return station; } + // Add an off-station base slot + public void addVirtualBaseSlot() { + baseSlots.add(1); + } public String getName() { return "Station " + id + " on " + tile.getClass().getSimpleName() + " " @@ -140,7 +145,7 @@ public int getNumber() { * @return Returns the baseSlots. */ public int getBaseSlots() { - return baseSlots; + return baseSlots.value(); } /** diff --git a/src/main/java/net/sf/rails/game/Stop.java b/src/main/java/net/sf/rails/game/Stop.java index 859665ce5..386e6edb8 100644 --- a/src/main/java/net/sf/rails/game/Stop.java +++ b/src/main/java/net/sf/rails/game/Stop.java @@ -264,6 +264,8 @@ public String getMutexId() { return mutexId; } + public int getValue() { return getRelatedStation().getValue(); } + public boolean isRunToAllowedFor(PublicCompany company, boolean running) { diff --git a/src/main/java/net/sf/rails/game/Stops.java b/src/main/java/net/sf/rails/game/Stops.java new file mode 100644 index 000000000..bc455565f --- /dev/null +++ b/src/main/java/net/sf/rails/game/Stops.java @@ -0,0 +1,126 @@ +package net.sf.rails.game; + +import java.util.HashMap; +import java.util.Map; + +/** + * Stops is a class intended to provide a central place + * for calculation of revenues per stop, possibly dependent + * on phase, tokening and train type. + * + * It was created out of frustration with the existing train run + * calculation code, that made handling train run revenues in 18VA + * a matter of building a very complex dynamic modifier. + * + * Another factor was the mine revenue in 1837 and 18VA, which + * wholly or partly went directly to the company treasury, + * completely separate from normal revenue. This has led to + * add another class called Revenue, which can transport both + * normal and special revenues, and is extendable. + * + * This class is a default version, that returns the fixed + * stop values that are sufficient in most games, an exception + * being the phase-dependent offmap revenues. + * + * Game-specific subclasses can be created to handle the kind + * of special cases that are so abundant in 18VA. + * + * The main usage of the methods of this class is expected to be + * by NetworkVertex and dynamic modifier objects. + * + * The results can be obtained in two ways: + * - as TOTAL REVENUE, which is the total revenue of a stop; + * - as EXTRA REVENUE, which is the correction to the standard + * revenue that is configured for tiles and offmap hexes. + * That correction (default zero) must be returned by the method + * predictionValue() of any dynamic modifier. + * + * Created 04/2023 by Erik Vos + */ +public class Stops { + + protected GameManager gameManager; + protected PhaseManager phaseManager; + protected MapManager mapManager; + + protected static Map instances = new HashMap<>(); + + protected Stops (RailsRoot root) { + gameManager = root.getGameManager(); + phaseManager = root.getPhaseManager(); + mapManager = root.getMapManager(); + } + + protected static Stops getInstance (RailsRoot root) { + if (!instances.containsKey(root)) { + instances.put(root, new Stops(root)); + } + return instances.get(root); + } + + public static Revenue getValue(Stop stop) { + return getInstance (stop.getRoot()).getRevenue(stop, null, null); + } + + public static Revenue getValueForTrain(Stop stop, Train train) { + return getInstance (stop.getRoot()).getRevenue(stop, train, null); + } + + public static Revenue getValueForTrainAndCompany (Stop stop, Train train, PublicCompany company) { + return getInstance (stop.getRoot()).getRevenue(stop, train, company); + } + + public static Revenue getExtraValue(Stop stop) { + return getInstance (stop.getRoot()).getExtraRevenue(stop, null, null); + } + + public static Revenue getExtraValueForTrain(Stop stop, Train train) { + return getInstance (stop.getRoot()).getExtraRevenue(stop, train, null); + } + + public static Revenue getExtraValueForTrainAndCompany (Stop stop, Train train, PublicCompany company) { + return getInstance (stop.getRoot()).getExtraRevenue(stop, train, company); + } + + public static int getTotalRevenue (Stop stop, Train train, PublicCompany company) { + Revenue revenue = getValueForTrainAndCompany (stop, train, company); + return revenue.getNormalRevenue() + revenue.getSpecialRevenue(); + } + + public static int getTotalExtraRevenue (Stop stop, Train train, PublicCompany company) { + Revenue revenue = getExtraValueForTrainAndCompany (stop, train, company); + return revenue.getNormalRevenue() + revenue.getSpecialRevenue(); + } + + + /* This default method can be overridden for any game + * in a game-specific subclass + * + * @param stop The stop for which a revenue value is requested + * @param train A specific train that may affect the revenue, + * or null if the train does not matter + * @param company A specific company that may affect the revenue, + * or null if the company does not matter + * @return A new Revenue object + */ + protected Revenue getRevenue (Stop stop, Train train, PublicCompany company) { + //if (stop.getType() == Stop.Type.OFFMAP) { + return new Revenue ( + stop.getValueForPhase(phaseManager.getCurrentPhase()), + 0); + //} else { + // return new Revenue (stop.getValue(), + // 0); + //} + + } + + /** Same as getRevenue(), but intended to get + * additional revenue vales, as are needed for + * the predictionValue() methods in dynamic modifiers. + */ + protected Revenue getExtraRevenue (Stop stop, Train train, PublicCompany company) { + return new Revenue (0, 0); + } + +} diff --git a/src/main/java/net/sf/rails/game/model/BaseTokensModel.java b/src/main/java/net/sf/rails/game/model/BaseTokensModel.java index 66ed6d79f..2d90a1c9f 100644 --- a/src/main/java/net/sf/rails/game/model/BaseTokensModel.java +++ b/src/main/java/net/sf/rails/game/model/BaseTokensModel.java @@ -46,7 +46,7 @@ public void initBaseTokens(SortedSet tokens) { /** * Add more tokens than the initially configured number. - * This is required for 1826. + * This is required for 1826 and 18VA * @param token A newly created token * @param laid True if the new token will immediately be laid, * i.e. it will never be a freely layable token. diff --git a/src/main/java/net/sf/rails/game/model/PortfolioModel.java b/src/main/java/net/sf/rails/game/model/PortfolioModel.java index f10c352ab..f2988acdd 100644 --- a/src/main/java/net/sf/rails/game/model/PortfolioModel.java +++ b/src/main/java/net/sf/rails/game/model/PortfolioModel.java @@ -467,9 +467,8 @@ public List getSpecialProperties(Class clazz, if ((clazz == null || clazz.isAssignableFrom(sp.getClass())) && sp.isExecutionable() && (!sp.isExercised() || includeExercised) - && (getParent() instanceof Company - && sp.isUsableIfOwnedByCompany() || getParent() instanceof Player - && sp.isUsableIfOwnedByPlayer())) { + && (getParent() instanceof Company && sp.isUsableIfOwnedByCompany() + || getParent() instanceof Player && sp.isUsableIfOwnedByPlayer())) { log.debug("Portfolio {} has SP {}", getParent().getId(), sp); result.add((T) sp); } @@ -481,9 +480,8 @@ public List getSpecialProperties(Class clazz, if ((clazz == null || clazz.isAssignableFrom(sp.getClass())) && sp.isExecutionable() && (!sp.isExercised() || includeExercised) - && (getParent() instanceof Company - && sp.isUsableIfOwnedByCompany() || getParent() instanceof Player - && sp.isUsableIfOwnedByPlayer())) { + && (getParent() instanceof Company && sp.isUsableIfOwnedByCompany() + || getParent() instanceof Player && sp.isUsableIfOwnedByPlayer())) { log.debug("Portfolio {} has persistent SP {}", getParent().getId(), sp); result.add((T) sp); } diff --git a/src/main/java/net/sf/rails/game/special/SpecialBaseTokenLay.java b/src/main/java/net/sf/rails/game/special/SpecialBaseTokenLay.java index 05ab1009a..7bbb108f4 100644 --- a/src/main/java/net/sf/rails/game/special/SpecialBaseTokenLay.java +++ b/src/main/java/net/sf/rails/game/special/SpecialBaseTokenLay.java @@ -22,6 +22,9 @@ public class SpecialBaseTokenLay extends SpecialProperty { private boolean requiresTile = false; private boolean requiresNoTile = false; private Forced forced = Forced.NO; + // Two specials for 18VA, where both will be set to true + private boolean create = false; + private boolean offCity = false; public enum Forced { NO, @@ -57,6 +60,9 @@ public void configureFromXML(Tag tag) throws ConfigurationException { requiresTile = tokenLayTag.getAttributeAsBoolean("requiresTile", requiresTile); requiresNoTile = tokenLayTag.getAttributeAsBoolean("requiresNoTile", requiresNoTile); forcedText = tokenLayTag.getAttributeAsString("forced", null); + // For 18VA + create = tokenLayTag.getAttributeAsBoolean("create", create); + offCity = tokenLayTag.getAttributeAsBoolean("offCity", offCity); description = LocalText.getText("LayBaseTokenInfo", connected ? LocalText.getText("aconnected") @@ -111,6 +117,22 @@ public String getLocationCodeString() { return locationCodes; } + public boolean isCreate() { + return create; + } + + public void setCreate(boolean create) { + this.create = create; + } + + public boolean isOffCity() { + return offCity; + } + + public void setOffCity(boolean offCity) { + this.offCity = offCity; + } + // That's rather overdoing revealing descriptions, here below. @Override public String toText() { diff --git a/src/main/java/net/sf/rails/game/special/SpecialRight.java b/src/main/java/net/sf/rails/game/special/SpecialRight.java index 31f74e9e9..cca09f7e2 100644 --- a/src/main/java/net/sf/rails/game/special/SpecialRight.java +++ b/src/main/java/net/sf/rails/game/special/SpecialRight.java @@ -96,10 +96,7 @@ public int getCost() { } public boolean isExecutionable() { - // FIXME: Check if this works correctly - // IT is better to rewrite this check - // see ExchangeForShare - return ((PrivateCompany)originalCompany).getOwner() instanceof Player; + return true; } /** diff --git a/src/main/java/net/sf/rails/game/special/SpecialTrainBuy.java b/src/main/java/net/sf/rails/game/special/SpecialTrainBuy.java index d49e13e1b..74bbe5573 100644 --- a/src/main/java/net/sf/rails/game/special/SpecialTrainBuy.java +++ b/src/main/java/net/sf/rails/game/special/SpecialTrainBuy.java @@ -52,6 +52,7 @@ public void configureFromXML(Tag tag) throws ConfigurationException { relativeDeduction = true; deductionAmountString = deductionString.replaceAll("%", ""); } else { + absoluteDeduction = true; deductionAmountString = deductionString; } try { diff --git a/src/main/java/net/sf/rails/game/specific/_1825/PublicCompany_1825.java b/src/main/java/net/sf/rails/game/specific/_1825/PublicCompany_1825.java index 5c6001a80..5bb11d597 100644 --- a/src/main/java/net/sf/rails/game/specific/_1825/PublicCompany_1825.java +++ b/src/main/java/net/sf/rails/game/specific/_1825/PublicCompany_1825.java @@ -21,7 +21,7 @@ public void setFormationOrderIndex(int formationOrderIndex) { } @Override - public void payout(int amount) { + public void adjustPriceOnPayout(int amount) { if (amount == 0) return; //Get current price int curSharePrice = currentPrice.getPrice().getPrice(); diff --git a/src/main/java/net/sf/rails/game/specific/_1826/OperatingRound_1826.java b/src/main/java/net/sf/rails/game/specific/_1826/OperatingRound_1826.java index 589a87c82..24ad47e51 100644 --- a/src/main/java/net/sf/rails/game/specific/_1826/OperatingRound_1826.java +++ b/src/main/java/net/sf/rails/game/specific/_1826/OperatingRound_1826.java @@ -180,7 +180,7 @@ protected void setGameSpecificPossibleActions() { } } - if (getStep() == GameDef.OrStep.REPAY_LOANS) { + if (getStep() == GameDef.OrStep.REPAY_LOANS) { // The possibility has already been checked in gameSpecificNextStep() if (!repayableLoans.isEmpty()) { diff --git a/src/main/java/net/sf/rails/game/specific/_1826/PublicCompany_1826.java b/src/main/java/net/sf/rails/game/specific/_1826/PublicCompany_1826.java index 4a7cf589e..e17af92f2 100644 --- a/src/main/java/net/sf/rails/game/specific/_1826/PublicCompany_1826.java +++ b/src/main/java/net/sf/rails/game/specific/_1826/PublicCompany_1826.java @@ -114,7 +114,7 @@ public void setFloated() { for (int i = 0; i < extraTokens; i++) { baseTokens.addBaseToken(BaseToken.create(this), false); } - numberOfBaseTokens += extraTokens; + numberOfBaseTokens.add(extraTokens); super.setFloated(); @@ -149,49 +149,7 @@ public boolean grow (boolean checkDestination) { if (!validateGrow(checkDestination)) return false; - growStep.add(1); - setShareUnit(shareUnitSizes.get(growStep.value())); - - BankPortfolio reserved = getRoot().getBank().getUnavailable(); - BankPortfolio ipo = getRoot().getBank().getIpo(); - Set last5Shares = reserved.getPortfolioModel().getCertificates(this); - for (PublicCertificate cert : last5Shares) { - if (hasStarted()) { - cert.moveTo(this); - } else { - // Still in IPO, put the reserved shares there too - cert.moveTo(ipo); - } - } - - ReportBuffer.add(this, LocalText.getText("CompanyHasGrown", - this, getActiveShareCount())); - - currentTrainLimits.setTo(trainLimits.get(growStep.value())); - ReportBuffer.add(this, - LocalText.getText("PhaseDependentTrainLimitsSetTo", - this, currentTrainLimits.view(), getCurrentTrainLimit())); - - - // For some reason the shareUnit change does not update - // the percentages shown in the GameStatus window. - // E.g. 60% should become 30%, etc. - // There must be a nicer way to accomplish that, - // but for now the below code works. - Set modelsToUpdate = new HashSet<>(); - PortfolioOwner owner; - Model model; - for (PublicCertificate cert : getCertificates()) { - owner = (PortfolioOwner) cert.getOwner(); - model = owner.getPortfolioModel().getShareModel(this); - if (!modelsToUpdate.contains(model)) modelsToUpdate.add(model); - } - for (Model m : modelsToUpdate) { - for (Observer obs : m.getObservers()) { - obs.update(m.toText()); - } - } - return true; + return super.grow(); } protected boolean validateGrow(boolean checkDestination) { diff --git a/src/main/java/net/sf/rails/game/specific/_1837/OperatingRound_1837.java b/src/main/java/net/sf/rails/game/specific/_1837/OperatingRound_1837.java index 2566f36fb..470677207 100644 --- a/src/main/java/net/sf/rails/game/specific/_1837/OperatingRound_1837.java +++ b/src/main/java/net/sf/rails/game/specific/_1837/OperatingRound_1837.java @@ -326,6 +326,7 @@ protected int calculateShareholderPayout (double payoutPerShare, int numberOfSha /* (non-Javadoc) * @see net.sf.rails.game.OperatingRound#gameSpecificTileLayAllowed(net.sf.rails.game.PublicCompany, net.sf.rails.game.MapHex, int) */ + /* @Override protected int processSpecialRevenue(int earnings, int specialRevenue) { int dividend = earnings; @@ -342,7 +343,7 @@ protected int processSpecialRevenue(int earnings, int specialRevenue) { } company.setLastDividend(dividend); return dividend; - } + }*/ @Override protected boolean gameSpecificTileLayAllowed(PublicCompany company, diff --git a/src/main/java/net/sf/rails/game/specific/_1880/OperatingRound_1880.java b/src/main/java/net/sf/rails/game/specific/_1880/OperatingRound_1880.java index fd1c8d75f..d405c3161 100644 --- a/src/main/java/net/sf/rails/game/specific/_1880/OperatingRound_1880.java +++ b/src/main/java/net/sf/rails/game/specific/_1880/OperatingRound_1880.java @@ -585,7 +585,7 @@ public void payout(int amount) { } // Move the token - operatingCompany.value().payout(amount); + operatingCompany.value().adjustPriceOnPayout(amount); } diff --git a/src/main/java/net/sf/rails/game/specific/_1880/PublicCompany_1880.java b/src/main/java/net/sf/rails/game/specific/_1880/PublicCompany_1880.java index 998dade75..a8b46cf87 100644 --- a/src/main/java/net/sf/rails/game/specific/_1880/PublicCompany_1880.java +++ b/src/main/java/net/sf/rails/game/specific/_1880/PublicCompany_1880.java @@ -130,7 +130,7 @@ public void withhold(int amount) { } @Override - public void payout(int amount) { + public void adjustPriceOnPayout(int amount) { if (canStockPriceMove.value() == true) { getRoot().getStockMarket().payOut(this); } diff --git a/src/main/java/net/sf/rails/game/specific/_18EU/AlpineTokenRevenueModifier.java b/src/main/java/net/sf/rails/game/specific/_18EU/AlpineTokenRevenueModifier.java index d72269ef9..b491959e6 100644 --- a/src/main/java/net/sf/rails/game/specific/_18EU/AlpineTokenRevenueModifier.java +++ b/src/main/java/net/sf/rails/game/specific/_18EU/AlpineTokenRevenueModifier.java @@ -68,9 +68,6 @@ public int evaluationValue(List runs, boolean optimalRuns) { //log.info("+++++ Checking run of train {} (optimal={})", run.getTrain(), optimalRuns); for (NetworkVertex vertex : run.getRunVertices()) { hex = vertex.getHex(); - if (hex == null) { - log.info ("????? Hex = null"); - } if (alpineTokenHexes.contains(hex)) { runHasAlpineToken = true; //log.info ("+++++ {} has an Alpine token on hex {}, tile={}", company, hex, hex.getCurrentTile()); diff --git a/src/main/java/net/sf/rails/game/specific/_18VA/GameDef_18VA.java b/src/main/java/net/sf/rails/game/specific/_18VA/GameDef_18VA.java index 4368c55d4..6314f1d93 100644 --- a/src/main/java/net/sf/rails/game/specific/_18VA/GameDef_18VA.java +++ b/src/main/java/net/sf/rails/game/specific/_18VA/GameDef_18VA.java @@ -1,5 +1,7 @@ package net.sf.rails.game.specific._18VA; +import java.util.Map; + /** * Externalised constants for 18VA */ @@ -7,5 +9,20 @@ public class GameDef_18VA { public final static String GOODS = "GOODS"; + public final static String BO = "B&O"; + + // Phases + public final static String PHASE_5 = "5"; + + // Port cities + // Note: this only works because all involved hexes have only one stop + public final static Map citiesWithPorts = Map.of ( + "C8","D9", + "E8","F9", + "M8","N9", + "O8","P9" + ); + + } diff --git a/src/main/java/net/sf/rails/game/specific/_18VA/GameManager_18VA.java b/src/main/java/net/sf/rails/game/specific/_18VA/GameManager_18VA.java index c514bd0d2..66edfa91e 100644 --- a/src/main/java/net/sf/rails/game/specific/_18VA/GameManager_18VA.java +++ b/src/main/java/net/sf/rails/game/specific/_18VA/GameManager_18VA.java @@ -3,6 +3,7 @@ import net.sf.rails.common.GuiDef; import net.sf.rails.game.GameManager; import net.sf.rails.game.RailsRoot; +import net.sf.rails.game.Train; public class GameManager_18VA extends GameManager { @@ -18,4 +19,13 @@ public void setGuiParameters() { } + /** Calculate value of a CMD */ + public int getValuePerTrain (Train train) { + if (train.getType().getCategory().equalsIgnoreCase("goods")) { + return 20 * train.getType().getMajorStops(); + } else { + return 0; + } + } + } diff --git a/src/main/java/net/sf/rails/game/specific/_18VA/OperatingRound_18VA.java b/src/main/java/net/sf/rails/game/specific/_18VA/OperatingRound_18VA.java new file mode 100644 index 000000000..a38320504 --- /dev/null +++ b/src/main/java/net/sf/rails/game/specific/_18VA/OperatingRound_18VA.java @@ -0,0 +1,102 @@ +package net.sf.rails.game.specific._18VA; + +import net.sf.rails.game.*; +import net.sf.rails.game.special.SpecialBaseTokenLay; +import net.sf.rails.game.special.SpecialRight; +import net.sf.rails.game.specific._1826.PublicCompany_1826; +import rails.game.action.GrowCompany; +import rails.game.action.LayBaseToken; +import rails.game.action.UseSpecialProperty; + +import java.util.ArrayList; +import java.util.List; + +public class OperatingRound_18VA extends OperatingRound { + + /** + * Constructed via Configure + */ + public OperatingRound_18VA(GameManager parent, String id) { + super(parent, id); + + } + + @Override + protected void setGameSpecificPossibleActions() { + + PublicCompany_18VA company = (PublicCompany_18VA) getOperatingCompany(); + + // From phase 3, 5-share companies may grow to 10-share companies + if (company.getShareUnit() == 20 + && getRoot().getPhaseManager().hasReachedPhase("3")) { + possibleActions.add(new GrowCompany(getRoot(), 10)); + } + + // Check if the operating company can use the extra train right + List srs = company.getPortfolioModel() + .getSpecialProperties(SpecialRight.class, false); + if (srs != null && !srs.isEmpty()) { + possibleActions.add(new UseSpecialProperty(srs.get(0))); + } + } + + @Override + protected void setSpecialTokenLays() { + + /* Special-property base token lays */ + currentSpecialTokenLays.clear(); + + PublicCompany company = operatingCompany.value(); + if (!company.canUseSpecialProperties()) return; + List remainingLocations = new ArrayList<>(); + + for (SpecialBaseTokenLay stl : getSpecialProperties(SpecialBaseTokenLay.class)) { + // in 18VA, below settings must be true, but check anyway + if (stl.isExtra() && stl.isCreate() && stl.isOffCity()) { + + // This STL is location specific. Check if there + // isn't already a token of this company + List locations = stl.getLocations(); + if (locations != null && !locations.isEmpty()) { + for (MapHex location : locations) { + if (location.hasTokenOfCompany(company)) { + continue; + } + remainingLocations.add(location); + } + } + LayBaseToken action = new LayBaseToken(getRoot(), stl); + action.setType(LayBaseToken.NON_CITY); + currentSpecialTokenLays.add(action); + } + } + } + + public boolean layBaseToken(LayBaseToken action) { + + if (action.getType() == LayBaseToken.NON_CITY) { + // Create an extra (virtual) token spot (in 18VA only 1 station per hex) + action.getChosenHex().getStation(1).addVirtualBaseSlot(); + // Create an extra token + //getOperatingCompany().getBaseTokensModel().addBaseToken(BaseToken.create(getOperatingCompany()), true); + ((PublicCompany_18VA)getOperatingCompany()).addBaseToken(); + } + + return super.layBaseToken(action); + } + + protected void newPhaseChecks() { + Phase phase = Phase.getCurrent(this); + String phaseId = phase.getId(); + + if (phaseId.equals("5")) { + // Convert all remaining 5-share companies to 10-share + for (PublicCompany company : companyManager.getAllPublicCompanies()) { + if (!company.isClosed() && company.getShareUnit() == 20) { + company.grow(); + } + } + } + } + +} diff --git a/src/main/java/net/sf/rails/game/specific/_18VA/PublicCompany_18VA.java b/src/main/java/net/sf/rails/game/specific/_18VA/PublicCompany_18VA.java new file mode 100644 index 000000000..4450a302d --- /dev/null +++ b/src/main/java/net/sf/rails/game/specific/_18VA/PublicCompany_18VA.java @@ -0,0 +1,186 @@ +package net.sf.rails.game.specific._18VA; + +import net.sf.rails.common.LocalText; +import net.sf.rails.common.ReportBuffer; +import net.sf.rails.common.parser.ConfigurationException; +import net.sf.rails.common.parser.Tag; +import net.sf.rails.game.BaseToken; +import net.sf.rails.game.PublicCompany; +import net.sf.rails.game.RailsItem; +import net.sf.rails.game.RailsRoot; +import net.sf.rails.game.financial.Bank; +import net.sf.rails.game.financial.BankPortfolio; +import net.sf.rails.game.financial.PublicCertificate; +import net.sf.rails.game.model.PortfolioOwner; +import net.sf.rails.game.special.ExtraTrainRight; +import net.sf.rails.game.specific._1826.GameDef_1826; +import net.sf.rails.game.state.Model; +import net.sf.rails.game.state.Observer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.HashSet; +import java.util.Set; + +public class PublicCompany_18VA extends PublicCompany { + + private static final Logger log = LoggerFactory.getLogger(PublicCompany_18VA.class); + + public PublicCompany_18VA(RailsItem parent, String id) { + super (parent, id, true); + } + + public PublicCompany_18VA(RailsItem parent, String id, boolean hasStockPrice) { + super(parent, id, hasStockPrice); + } + + // Probably redundant + public void configureFromXML(Tag tag) throws ConfigurationException { + + super.configureFromXML(tag); + + } + + + // Really needed? + public void finishConfiguration(RailsRoot root) + throws ConfigurationException { + + super.finishConfiguration(root); + + // 5-share companies have an initial share unit of 20% + if (isPotentialFiveShareCompany()) { + setShareUnit(20); + } + } + + private boolean isPotentialFiveShareCompany() { + return getType().getId().equals("Public") && !getId().equals(GameDef_18VA.BO); + } + + /** Check if a company must get more tokens that the configured minimal number.*/ + @Override + public void setFloated() { + + int extraTokens = 0; + boolean reachedPhase5 = getType().getId().equals("Public") + && getRoot().getPhaseManager().hasReachedPhase(GameDef_18VA.PHASE_5); + + if (reachedPhase5) { + extraTokens += 2; + } + + for (int i = 0; i < extraTokens; i++) { + baseTokens.addBaseToken(BaseToken.create(this), false); + } + numberOfBaseTokens.add (extraTokens); + + super.setFloated(); + Set certs = getRoot().getBank().getIpo() + .getPortfolioModel().getCertificates(this); + for (PublicCertificate cert : certs) { + cert.moveTo(this); + }; + + + // TODO + if (reachedPhase5) { + + + } + } + + public void addBaseToken () { + baseTokens.addBaseToken(BaseToken.create(this), false); + numberOfBaseTokens.add(1); + } + + // Probably not needed + protected void setCapitalizationShares() { + if (getId().equals(GameDef_1826.SNCF)) { + capitalisationShares = getPortfolioModel().getShares(this); + } else if (getId().equals(GameDef_1826.ETAT)) { + capitalisationShares = 0; + } + log.debug("{} CapFactor set to {}", this, capitalisationShares); + } + + @Override // Probably not needed + public int getCapitalisation() { + if (getType().getId().equalsIgnoreCase("Public") + && getRoot().getPhaseManager().hasReachedPhase(GameDef_18VA.PHASE_5)) { + return CAPITALISE_FULL; + } else { + return capitalisation; + } + } + + /** Convert company from a 5-share to a 10-share company */ + /* The intention is to make this code usable for other games as well. */ + public boolean grow (boolean checkDestination) { + + if (!validateGrow(checkDestination)) return false; + + growStep.add(1); + setShareUnit(shareUnitSizes.get(growStep.value())); + + BankPortfolio reserved = getRoot().getBank().getUnavailable(); + BankPortfolio ipo = getRoot().getBank().getIpo(); + Set last5Shares = reserved.getPortfolioModel().getCertificates(this); + for (PublicCertificate cert : last5Shares) { + if (hasStarted()) { + cert.moveTo(this); + } else { + // Still in IPO, put the reserved shares there too + cert.moveTo(ipo); + } + } + + ReportBuffer.add(this, LocalText.getText("CompanyHasGrown", + this, getActiveShareCount())); + + currentTrainLimits.setTo(trainLimits.get(growStep.value())); + ReportBuffer.add(this, + LocalText.getText("PhaseDependentTrainLimitsSetTo", + this, currentTrainLimits.view(), getCurrentTrainLimit())); + + + // For some reason the shareUnit change does not update + // the percentages shown in the GameStatus window. + // E.g. 60% should become 30%, etc. + // There must be a nicer way to accomplish that, + // but for now the below code works. + Set modelsToUpdate = new HashSet<>(); + PortfolioOwner owner; + Model model; + for (PublicCertificate cert : getCertificates()) { + owner = (PortfolioOwner) cert.getOwner(); + model = owner.getPortfolioModel().getShareModel(this); + if (!modelsToUpdate.contains(model)) modelsToUpdate.add(model); + } + for (Model m : modelsToUpdate) { + for (Observer obs : m.getObservers()) { + obs.update(m.toText()); + } + } + return true; + } + + protected boolean validateGrow(boolean checkDestination) { + return super.validateGrow() + && (!checkDestination || hasReachedDestination()); + } + + @Override + protected int getTrainLimit(int phaseIndex) { + + int limit = super.getTrainLimit(phaseIndex); + if (rightsModel == null || rightsModel.isEmpty()) return limit; + + ExtraTrainRight etr = rightsModel.getRightType(ExtraTrainRight.class); + if (etr != null) limit += etr.getExtraTrains(); + + return limit; + } + +} \ No newline at end of file diff --git a/src/main/java/net/sf/rails/game/specific/_18VA/RevenueManager_18VA.java b/src/main/java/net/sf/rails/game/specific/_18VA/RevenueManager_18VA.java new file mode 100644 index 000000000..8312ff830 --- /dev/null +++ b/src/main/java/net/sf/rails/game/specific/_18VA/RevenueManager_18VA.java @@ -0,0 +1,119 @@ +package net.sf.rails.game.specific._18VA; + +import com.google.common.collect.BiMap; +import com.google.common.collect.HashBiMap; +import net.sf.rails.algorithms.RevenueAdapter; +import net.sf.rails.algorithms.RevenueManager; +import net.sf.rails.common.LocalText; +import net.sf.rails.common.parser.ConfigurationException; +import net.sf.rails.game.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** This is a subclass of RevenueManager, only meant to adapt + * ultimate revenue calculation to games with non-standard rules. + * 18VA is an extreme case of such special rules. + * + * I believe that this way of calculating revenue obviates + * the need for providing additional 'prediction values' + * in dynamic modifiers. + * + * Created by Erik Vos 04/2023 + * */ +public class RevenueManager_18VA extends RevenueManager { + + private BiMap portCities; + private MapManager mapManager; + + private static final Logger log = LoggerFactory.getLogger(RevenueManager_18VA.class); + + public RevenueManager_18VA (RailsRoot parent, String id) { + + super(parent, id); + } + + public void finishConfiguration(RailsRoot parent) + throws ConfigurationException { + portCities = HashBiMap.create(); + mapManager = parent.getMapManager(); + Stop port, city; + for (String cityName : GameDef_18VA.citiesWithPorts.keySet()) { + city = mapManager.getHex(cityName).getStops().asList().get(0); + port = mapManager.getHex(GameDef_18VA.citiesWithPorts.get(cityName)).getStops().asList().get(0); + portCities.put (port, city); + } + log.debug("Related cities of ports {}", portCities); + } + + @Override + protected Revenue getBaseRevenue(Stop stop, Train train, PublicCompany company) { + + Revenue baseRev = new Revenue (0,0); + boolean isGoods = train.getCategory().equalsIgnoreCase("goods"); + boolean isTokened = stop.hasTokenOf(company); + Phase phase = phaseManager.getCurrentPhase(); + + switch (stop.getType()) { + case TOWN: // 'Mine' in 18VA parlance + if (isGoods) { + baseRev.addNormalRevenue(stop.getValue()); + } + break; + case CITY: + baseRev.addNormalRevenue(stop.getValueForPhase(phase)) + .multiplyRevenue(train.getCityScoreFactor()); + break; + case OFFMAP: + baseRev.addNormalRevenue(stop.getValueForPhase(phase)) + .multiplyRevenue(train.getCityScoreFactor()); // 4D scores double + if (isTokened) { + baseRev.multiplyRevenue(2); + } + break; + case MINE: // 'CMD' in 18VA parlance + if (isGoods) { // CMD must have configured value 20 - does not work?? + // FIXME: Why does stop.getValue() return 0 rather than the configured 20? + baseRev.addNormalRevenue (20 * train.getMajorStops()); + if (isTokened + && !phaseManager.hasReachedPhase("4D")) { + // Add treasury revenue as special revenue + baseRev.addSpecialRevenue(baseRev.getNormalRevenue()); + } + } + break; + case PORT: + Stop relatedCity = portCities.get(stop); + // No port revenue without a token in the connected city + if (relatedCity.hasTokenOf(company)) { + baseRev.addNormalRevenue(stop.getValueForPhase(phase)); + if (isGoods) { + baseRev.multiplyRevenue (2) + .addRevenue(relatedCity.getValue(), 0); + } + } + } + return baseRev; + } + + // Currently not used. + public Stop getCityOfPort (Stop port) { + return portCities.get(port); + } + + protected String prettyPrint(RevenueAdapter revenueAdapter) { + + String prettyPrint = super.prettyPrint (revenueAdapter); + + if (specialRevenue > 0){ + // Remove a redundant newline (source unknown) + // .replace() does not work (for unknown reasons) + prettyPrint = prettyPrint.stripTrailing() + "\n"; + int normalRevenue = revenueAdapter.getTotalRevenue() - specialRevenue; + prettyPrint += LocalText.getText("DivideEarnings", specialRevenue, normalRevenue); + } + return prettyPrint; + } + +} + + diff --git a/src/main/java/net/sf/rails/game/specific/_18VA/StartRound_18VA.java b/src/main/java/net/sf/rails/game/specific/_18VA/StartRound_18VA.java new file mode 100644 index 000000000..18ac3361a --- /dev/null +++ b/src/main/java/net/sf/rails/game/specific/_18VA/StartRound_18VA.java @@ -0,0 +1,368 @@ +package net.sf.rails.game.specific._18VA; + +import net.sf.rails.common.DisplayBuffer; +import net.sf.rails.common.LocalText; +import net.sf.rails.common.ReportBuffer; +import net.sf.rails.game.*; +import net.sf.rails.game.financial.Bank; +import net.sf.rails.game.financial.Certificate; +import net.sf.rails.game.financial.PublicCertificate; +import net.sf.rails.game.special.SpecialProperty; +import net.sf.rails.game.state.Currency; +import net.sf.rails.game.state.GenericState; +import net.sf.rails.game.state.MoneyOwner; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import rails.game.action.*; + +import java.util.Set; + +/** + * Implements an 1830-style initial auction. + */ +public class StartRound_18VA extends StartRound { + private static final Logger log = LoggerFactory.getLogger(net.sf.rails.game.specific._18VA.StartRound_18VA.class); + + protected final int bidIncrement; + protected Player firstPasser = null; + + public static final String boName = "B&O"; + public final PublicCompany bo = companyManager.getPublicCompany(boName); + Player boBuyer; + StartItem boItem; + + private final GenericState auctionItemState = + new GenericState<>(this, "auctionItemState"); + + /** + * Constructed via Configure + */ + public StartRound_18VA(GameManager parent, String id) { + super(parent, id); + bidIncrement = startPacket.getModulus(); + } + + @Override + public void start() { + super.start(); + auctionItemState.set(null); + setPossibleActions(); + } + + /* + public boolean process(PossibleAction action) { + + if (!super.process(action)) return false; + + if (action instanceof BuyStartItem && action.getPlayer().equals(boBuyer)) { + BuyStartItem bsi = (BuyStartItem) action; + int price = bsi.getAssociatedSharePrice(); + PublicCompany bo = ((PublicCertificate)bsi.getStartItem().getPrimary()).getCompany(); + log.info("B&O share price = {}", price); + } + return true; + }*/ + + /** + * In 1826, starting the B&O involves two steps: + * 1. The certificate, as a part of the Start Packet, is bought + * but not yet started. + * In this step, the action attribute 'companyNeedingSharePrice' + * is set to null, so no price request happens. + * This needs no further action here. + * 2. Only if the whole Start Packet has been sold, the price is + * requested and set. The B&O starts when this is complete. + * In this step, the B&O is started after setting the price. + */ + @Override + protected void checksOnBuying(Certificate cert, int sharePrice) { + if (cert instanceof PublicCertificate) { + PublicCertificate pubCert = (PublicCertificate) cert; + PublicCompany comp = pubCert.getCompany(); + if (!comp.equals(bo)) return; // Impossible + + // Step 1 + if (sharePrice == 0) return; + // Step 2 + comp.start(sharePrice); + } + } + + @Override + public boolean setPossibleActions() { + + boolean passAllowed = true; + + possibleActions.clear(); + + if (playerManager.getCurrentPlayer() == startPlayer) ReportBuffer.add(this, ""); + + // FIXME: Rails 2.0 Could be an infinite loop if there if no player has enough money to buy an item + while (possibleActions.isEmpty()) { + Player currentPlayer = playerManager.getCurrentPlayer(); + + for (StartItem item : itemsToSell.view()) { + + if (item.isSold()) { + // Don't include + } else if (item.getStatus() == StartItem.NEEDS_SHARE_PRICE) { + /* B&O */ + playerManager.setCurrentPlayer(item.getBidder()); + possibleActions.add(new BuyStartItem(item, item.getBid(), false, true)); + passAllowed = false; + break; // No more actions + } else { + int currentBid = item.getBid(); + item.setMinimumBid(currentBid > 0 ? currentBid + bidIncrement : item.getBasePrice()); + item.setStatus(StartItem.BIDDABLE); + if (currentPlayer.getFreeCash() + + item.getBid(currentPlayer) >= item.getMinimumBid()) { + BidStartItem possibleAction = + new BidStartItem(item, item.getMinimumBid(), + startPacket.getModulus(), false); + possibleActions.add(possibleAction); + } + } + + } + + if (possibleActions.isEmpty()) { + numPasses.add(1); + if (auctionItemState.value() == null) { + playerManager.setCurrentToNextPlayer(); + } else { + setNextBiddingPlayer(auctionItemState.value()); + } + } + } + + if (passAllowed) { + possibleActions.add(new NullAction(getRoot(), NullAction.Mode.PASS)); + } + + return true; + } + + /*----- moveStack methods -----*/ + + /** + * The current player bids on a given start item. + * + * @param playerName The name of the current player (for checking purposes). + * @param bidItem The start item on which the bid is placed. + */ + @Override + protected boolean bid(String playerName, BidStartItem bidItem) { + + StartItem item = bidItem.getStartItem(); + String errMsg = null; + Player player = playerManager.getCurrentPlayer(); + int previousBid = 0; + int bidAmount = bidItem.getActualBid(); + + while (true) { + + // Check player + if (!playerName.equals(player.getId())) { + errMsg = LocalText.getText("WrongPlayer", playerName, player.getId()); + break; + } + // Check item + boolean validItem = false; + for (StartItemAction activeItem : possibleActions.getType(StartItemAction.class)) { + if (bidItem.equalsAsOption(activeItem)) { + validItem = true; + break; + } + + } + if (!validItem) { + errMsg = LocalText.getText("ActionNotAllowed", + bidItem.toString()); + break; + } + + // Is the item buyable? + if (bidItem.getStatus() != StartItem.BIDDABLE + && bidItem.getStatus() != StartItem.AUCTIONED) { + errMsg = LocalText.getText("NotForSale"); + break; + } + + // Bid must be at least 5 above last bid + if (bidAmount < item.getMinimumBid()) { + errMsg = LocalText.getText("BidTooLow", "" + + item.getMinimumBid()); + break; + } + + // Bid must be a multiple of the modulus + if (bidAmount % startPacket.getModulus() != 0) { + errMsg = LocalText.getText("BidMustBeMultipleOf", + bidAmount, + startPacket.getMinimumIncrement()); + break; + } + + // Has the buyer enough cash? + previousBid = item.getBid(player); + int available = player.getFreeCash() + previousBid; + if (bidAmount > available) { + errMsg = LocalText.getText("BidTooHigh", Bank.format(this, available)); + break; + } + + break; + } + + if (errMsg != null) { + DisplayBuffer.add(this, LocalText.getText("InvalidBid", + playerName, + item.getId(), + errMsg)); + return false; + } + + + item.setBid(bidAmount, player); + if (previousBid > 0) player.unblockCash(previousBid); + player.blockCash(bidAmount); + ReportBuffer.add(this, LocalText.getText("BID_ITEM_LOG", + playerName, + Bank.format(this, bidAmount), + item.getId(), + Bank.format(this, player.getFreeCash()))); + + playerManager.setCurrentToNextPlayer(); + + numPasses.set(0); + + return true; + + } + + /** + * Process a player's pass. + * + * @param playerName The name of the current player (for checking purposes). + */ + @Override + protected boolean pass(NullAction action, String playerName) { + + String errMsg = null; + Player player = playerManager.getCurrentPlayer(); + + while (true) { + + // Check player + if (!playerName.equals(player.getId())) { + errMsg = LocalText.getText("WrongPlayer", playerName, player.getId()); + break; + } + break; + } + + if (errMsg != null) { + DisplayBuffer.add(this, LocalText.getText("InvalidPass", + playerName, + errMsg)); + return false; + } + + ReportBuffer.add(this, LocalText.getText("PASSES", playerName)); + + numPasses.add(1); + if (numPasses.value() == 1) firstPasser = player; + + if (numPasses.value() >= playerManager.getNumberOfPlayers()) { + // All players have passed. + gameManager.reportAllPlayersPassed(); + playerManager.setPriorityPlayer(firstPasser); + + // Assign all biddable items that have at least one bid + for (StartItem item : startPacket.getUnsoldItems()) { + log.debug("Unsold item: {} bid={}", item, item.getBid()); + player = item.getBidder(); + if (item.getBidder() != null) { + int price = item.getBid(); + assignItem(player, item, price, 0); + + } + } + if (!startPacket.getUnsoldItems().isEmpty()) { + // If any item has not been bid upon, reduce its price by 10. + for (StartItem item : startPacket.getUnsoldItems()) { + item.reduceBasePriceBy(10); + ReportBuffer.add(this, LocalText.getText( + "ITEM_PRICE_REDUCED", + startPacket.getFirstUnsoldItem().getId(), + Bank.format(this, startPacket.getFirstUnsoldItem().getBasePrice()))); + numPasses.set(0); + if (item.getBasePrice() == 0) { + // Assign it to the priority holder + assignItem(playerManager.getPriorityPlayer(), item, 0, 0); + playerManager.setPriorityPlayerToNext(); + } + } + } + if (startPacket.getUnsoldItems().isEmpty()) { + // Finish start round. B&O owner must now set the share price. + if (bo.hasFloated()) { + finishRound(); + } else { + playerManager.setCurrentPlayer(boBuyer); + boItem.setStatus(StartItem.NEEDS_SHARE_PRICE); + } + } + } else { + playerManager.setCurrentToNextPlayer(); + } + + return true; + } + + + private void setNextBiddingPlayer(StartItem item, Player biddingPlayer) { + for (Player player : playerManager.getNextPlayersAfter(biddingPlayer, false, false)) { + if (item.isActive(player)) { + playerManager.setCurrentPlayer(player); + break; + } + } + } + + private void setNextBiddingPlayer(StartItem item) { + setNextBiddingPlayer(item, playerManager.getCurrentPlayer()); + } + + /** See Javadoc of checksOnBuying() */ + protected void assignItem(Player player, StartItem item, int price, + int sharePrice) { + + if (item.getDisplayName().equals(boName)) { + if (sharePrice == 0) { + // Step 1 + boBuyer = player; + boItem = item; + super.assignItem(player, item, price, sharePrice); + //item.setStatus(StartItem.NEEDS_SHARE_PRICE); + log.debug ("B&O step 1 done"); + } else { + // Step 2 + bo.start(sharePrice); + Currency.fromBank(2 * sharePrice, bo); + bo.setFloated(); + boBuyer = null; + item.setStatus(StartItem.SOLD); + log.debug ("B&O step 2 is done"); + } + } else { + super.assignItem(player, item, price, sharePrice); + log.debug ("Item {} assigned to {}", item, player); + } + } +} + + + + diff --git a/src/main/java/net/sf/rails/game/specific/_18VA/StockRound_18VA.java b/src/main/java/net/sf/rails/game/specific/_18VA/StockRound_18VA.java new file mode 100644 index 000000000..834a0c3d9 --- /dev/null +++ b/src/main/java/net/sf/rails/game/specific/_18VA/StockRound_18VA.java @@ -0,0 +1,49 @@ +package net.sf.rails.game.specific._18VA; + +import net.sf.rails.common.LocalText; +import net.sf.rails.common.ReportBuffer; +import net.sf.rails.game.GameManager; +import net.sf.rails.game.PublicCompany; +import net.sf.rails.game.financial.PublicCertificate; +import net.sf.rails.game.financial.StockRound; +import net.sf.rails.game.state.Currency; +import rails.game.action.BuyCertificate; + +public class StockRound_18VA extends StockRound { + + public StockRound_18VA(GameManager parent, String id) { + super(parent, id); + } + + @Override + public boolean buyShares(String playerName, BuyCertificate action) { + + boolean result = super.buyShares (playerName, action); + + /* When the 6th share of a 10-share company is bought, + * all remaining shares go to the Pool, and the company + * is fully capitalised. + * + * Note: as this is part of a one-time player action, + * and companies cannot trade their own shares, + * there is no danger that this buy follow-up will be repeated. + */ + PublicCompany company = action.getCompany(); + if (company.getShareUnit() == 10 && company.getPortfolioModel().getShares(company) == 4) { + for (PublicCertificate cert : company.getPortfolioModel().getCertificates(company)) { + cert.moveTo(pool); + } + int cash = 4 * company.getMarketPrice(); + String cashText = Currency.fromBank(cash, company); + ReportBuffer.add(this, LocalText.getText("SELL_SHARES_LOG", + company, + 4, + company.getShareUnit(), + (4 * company.getShareUnit()), + company, + cashText)); + + } + return result; + } +} diff --git a/src/main/java/net/sf/rails/game/specific/_18VA/Stops_18VA.java b/src/main/java/net/sf/rails/game/specific/_18VA/Stops_18VA.java new file mode 100644 index 000000000..2f6b2e519 --- /dev/null +++ b/src/main/java/net/sf/rails/game/specific/_18VA/Stops_18VA.java @@ -0,0 +1,65 @@ +package net.sf.rails.game.specific._18VA; + +import net.sf.rails.game.*; + +import java.util.HashMap; +import java.util.Map; + +public class Stops_18VA extends Stops { + + private Map cityPorts = new HashMap<>(); + + protected Stops_18VA (RailsRoot root) { + super (root); + + Stop port, city; + for (String cityName : GameDef_18VA.citiesWithPorts.keySet()) { + city = mapManager.getHex(cityName).getStops().asList().get(0); + port = mapManager.getHex(GameDef_18VA.citiesWithPorts.get(cityName)).getStops().asList().get(0); + cityPorts.put (city, port); + } + } + + @Override + protected Revenue getRevenue(Stop stop, Train train, PublicCompany company) { + return super.getRevenue(stop, train, company) + .addRevenue(getExtraRevenue(stop, train, company)); + } + + @Override + protected Revenue getExtraRevenue(Stop stop, Train train, PublicCompany company) { + Revenue extraRev = new Revenue(0, 0); + boolean isGoods = train.getCategory().equalsIgnoreCase("goods"); + boolean isTokened = stop.hasTokenOf(company); + Phase phase = phaseManager.getCurrentPhase(); + switch (stop.getType()) { + case PORT: + // The port revenue is added to the neighbouring city + extraRev.addNormalRevenue(-stop.getValue()); + break; + case MINE: // CMD + if (isGoods) { + int factor = stop.hasTokenOf(company) ? 2 : 1; + extraRev.addSpecialRevenue (factor * 20 * train.getMajorStops()); + } + break; + case OFFMAP: + if (stop.hasTokenOf(company)) { + extraRev.addNormalRevenue(stop.getValueForPhase(phase)); + } + break; + case TOWN: // mine + if (isGoods) extraRev.addNormalRevenue(stop.getValue()); + break; + case CITY: + if (cityPorts.containsKey(stop) && isTokened) { + Stop port = cityPorts.get(stop); + int portValue = port.getValueForPhase(phase); + extraRev.addNormalRevenue(stop.getValue() + portValue * (isGoods ? 2 : 1)); + } + break; + default: + } + return extraRev; + } +} diff --git a/src/main/java/net/sf/rails/game/specific/_18VA/TrainRunModifier.java b/src/main/java/net/sf/rails/game/specific/_18VA/TrainRunModifier.java index efb7a4299..ab4f1278d 100644 --- a/src/main/java/net/sf/rails/game/specific/_18VA/TrainRunModifier.java +++ b/src/main/java/net/sf/rails/game/specific/_18VA/TrainRunModifier.java @@ -20,85 +20,24 @@ public class TrainRunModifier private static final Logger log = LoggerFactory.getLogger(TrainRunModifier.class); - private int cmdValue; - private int cmdDirectValue; - private int portExtraValue; - private int offMapExtraValue; private PublicCompany company; - private Phase phase; + private int cmdDirectValue; @Override public boolean prepareModifier(RevenueAdapter revenueAdapter) { company = revenueAdapter.getCompany(); - phase = revenueAdapter.getPhase(); cmdDirectValue = 0; return true; } - @Override public int predictionValue(List runs) { - int predictionValue = 0; - for (RevenueTrainRun run : runs) { - Train train = run.getTrain().getRailsTrain(); - boolean isGoods = train.getCategory().equalsIgnoreCase("goods"); - int majors = 0; - int value = 0; - for (NetworkVertex v : run.getRunVertices()) { - switch (v.getStop().getRelatedStation().getType()) { - case PORT: - NetworkVertex port = v; - NetworkVertex city = getCityOfPort(v, run); - //value += port.getValue(); - if (isGoods) { - value += port.getValue() + city.getValue(); - } - break; - case MINE: // CMD - if (isGoods) { - //int factor = v.getStop().hasTokenOf(company) ? 2 : 1; - if (v.getStop().hasTokenOf(company)) { - value += 20 * train.getMajorStops(); - } - } - break; - case OFFMAP: - //int factor = v.getStop().hasTokenOf(company) ? 2 : 1; - if (v.getStop().hasTokenOf(company)) { - value += v.getValue(); - } - majors++; - break; - case TOWN: // mine - //if (isGoods) value += v.getValue(); - break; - case CITY: - majors++; - default: - //value += v.getValue(); - } - } - if (majors >= (isGoods ? 1 : 2)) { // Otherwise no valid run - log.debug("Prediction for {} {} is {}", - run.getTrain().getRailsTrain(), run.getRunVertices(), value); - - predictionValue += value; - } else { - log.debug ("Prediction for {} {} is 0 - not a valid run", - run.getTrain().getRailsTrain(), run.getRunVertices()); - } - } - log.debug ("Total extra prediction={}", predictionValue); - return predictionValue; + return 0; } + // FIXME The value calculations should use RevenueManager_18VA. private List identifyInvalidRuns(List runs) { - cmdValue = 0; - //cmdDirectValue = 0; - portExtraValue = 0; - offMapExtraValue = 0; - List vertices; List invalidRuns = new ArrayList<>(); int i = 0; @@ -122,11 +61,7 @@ private List identifyInvalidRuns(List runs) { log.debug("Skipped: no run"); continue; } - /* - log.debug(">>> Run {} {}: {}", i, t, - run.prettyPrint(true) - .replaceAll("\\n+", "") - .replaceAll("\\s+", " "));*/ + String trainCategory = run.getTrain().getRailsTrain().getCategory(); if (!Util.hasValue(trainCategory)) { invalidRuns.add(run); @@ -165,7 +100,7 @@ private List identifyInvalidRuns(List runs) { if (firstStationType == Stop.Type.PORT || lastStationType == Stop.Type.PORT) { boolean portReached = false; NetworkVertex port = firstStationType == Stop.Type.PORT ? firstVertex : lastVertex; - NetworkVertex city = getCityOfPort (port, run); + NetworkVertex city = getCityOfPort(port, run); if (city != null && city.getStop().hasTokenOf(company) && majors >= 2) { // An 1G-train cannot reach a port, which does not count as a separate station portReached = true; @@ -174,9 +109,6 @@ private List identifyInvalidRuns(List runs) { invalidRuns.add(run); log.debug("Skipped: port not reached"); continue; - } else if (isGoods && port != null && city != null) { - // Calculate extra port value - portExtraValue += port.getValue() + city.getValue(); } } @@ -189,14 +121,11 @@ private List identifyInvalidRuns(List runs) { continue; } cmdDirectValue = 0; - log.debug(">>>>> DirRev set to 0"); directValueReset = false; } Stop cmdStop = (firstStationType == Stop.Type.MINE ? firstStop : lastStop); int trainLevel = run.getTrain().getRailsTrain().getMajorStops(); - //int baseValue = trainLevel * cmdStop.getHex().getCurrentValueForPhase(phase); // 20 int baseValue = trainLevel * 20; - cmdValue += baseValue; if (isGoods && cmdStop.hasTokenOf(company)) { // Calculate CMD direct value (i.e. what goes into treasury) cmdDirectValue += baseValue; @@ -205,24 +134,11 @@ private List identifyInvalidRuns(List runs) { // Temporary fixture to keep passenger trains off towns (i.e. mines) if (!isGoods && (firstStationType == Stop.Type.TOWN || lastStationType == Stop.Type.TOWN)) { - invalidRuns.add (run); + invalidRuns.add(run); log.debug("Skipped: {} wrong category to mine (town)", trainCategory); continue; } - - // Calculate extra OffMap value - if (firstStationType == Stop.Type.OFFMAP || lastStationType == Stop.Type.OFFMAP - && !phase.getId().equalsIgnoreCase("4D")) { - Stop offMapStop = (firstStationType == Stop.Type.OFFMAP ? firstStop : lastStop); - if (offMapStop.hasTokenOf(company)) { - // Calculate offmap extra value - offMapExtraValue += offMapStop.getHex().getCurrentValueForPhase(phase); - } - } } - log.debug("After run validation: port={} cmd={} cmdDirect={} offmap={}", - portExtraValue, cmdValue, cmdDirectValue, offMapExtraValue); - return invalidRuns; } @@ -235,10 +151,7 @@ public int evaluationValue(List runs, boolean optimalRuns) { for (RevenueTrainRun run:identifyInvalidRuns(runs)) { changeRevenues -= run.getRunValue(); } - log.debug("Eval: inv={} port={} cmd={} direct={} off={}", - changeRevenues, portExtraValue, cmdValue, cmdDirectValue, offMapExtraValue); - // Note: total revenue must include direct revenue, which will be subtracted later - return changeRevenues + portExtraValue + cmdValue + cmdDirectValue + offMapExtraValue; + return 0; } @Override @@ -273,36 +186,29 @@ public int getSpecialRevenue () { @Override public String prettyPrint(RevenueAdapter adapter) { - StringBuilder b = new StringBuilder(""); - if (portExtraValue != 0) b.append("Port bonus = ").append(portExtraValue); - if (cmdValue != 0) b.append(b.length() > 0 ? ", " : "") - .append("CMD value = ").append(cmdValue); - if (cmdDirectValue != 0) b.append(b.length() > 0 ? ", " : "") - .append("CMD treasury income = ").append(cmdDirectValue); - if (offMapExtraValue != 0) b.append(b.length() > 0 ? ", " : "") - .append("OffMap bonus = ").append(offMapExtraValue); - return b.length() > 0 ? b.toString() : null; + return ""; } + + // Should if possible be merged with the similar method in RevenueManager_18VA. + // That one uses stops, this one vertices. Not sure what is better. private NetworkVertex getCityOfPort (NetworkVertex port, RevenueTrainRun run) { - if (port.getStop().getRelatedStation().getType() != Stop.Type.PORT) { + if (port.getStop().getType() != Stop.Type.PORT) { log.debug ("Error: {} is not a Port!", port); return null; } List portVertices = new ArrayList<>(run.getRunVertices()); - NetworkVertex city = null; + if (run.getLastVertex() == port) { Collections.reverse(portVertices); } for (NetworkVertex vertex : portVertices) { if (!vertex.isSide() - && vertex.getStop().getRelatedStation().getType() == Stop.Type.CITY) { + && vertex.getStop().getType() == Stop.Type.CITY) { // This must be the city where the port belongs to - city = vertex; - log.debug("Found city {} for port {}", city.getStop(), port.getStop()); - break; + return vertex; } } - return city; + return null; } } diff --git a/src/main/java/net/sf/rails/game/state/Ownable.java b/src/main/java/net/sf/rails/game/state/Ownable.java index fc4151fc2..d958a4e37 100644 --- a/src/main/java/net/sf/rails/game/state/Ownable.java +++ b/src/main/java/net/sf/rails/game/state/Ownable.java @@ -1,6 +1,6 @@ package net.sf.rails.game.state; -public interface Ownable extends Item, Comparable { +public interface Ownable extends Item, Comparable { /** * Moves the ownable (item) to the new owner diff --git a/src/main/java/net/sf/rails/tools/ListAndFixSavedFiles.java b/src/main/java/net/sf/rails/tools/ListAndFixSavedFiles.java index 568fcbe5a..52bc893f6 100644 --- a/src/main/java/net/sf/rails/tools/ListAndFixSavedFiles.java +++ b/src/main/java/net/sf/rails/tools/ListAndFixSavedFiles.java @@ -379,7 +379,9 @@ private void save() { private void edit(int index) { editedAction = gameLoader.getActions().get(index); editedIndex = index; - if (editedAction instanceof BuyTrain) { + if (editedAction instanceof BidStartItem) { + new BidStartItemDialog ((BidStartItem) editedAction); + } else if (editedAction instanceof BuyTrain) { new BuyTrainDialog ((BuyTrain) editedAction); } else if (editedAction instanceof LayTile) { new LayTileDialog((LayTile) editedAction); @@ -473,6 +475,51 @@ public void actionPerformed(ActionEvent arg0) { abstract PossibleAction processInput(); } + private class BidStartItemDialog extends EditDialog { + private static final long serialVersionUID = 1L; + private BidStartItem action; + + BidStartItemDialog (BidStartItem action) { + super ("Edit BidStartItem"); + this.action = action; + addTextField (this, "Minimum bid", + action.getMinimumBid(), + String.valueOf(action.getMinimumBid())); // 0 + addTextField (this, "Bid increment", + action.getBidIncrement(), + String.valueOf(action.getBidIncrement())); // 1 + addTextField (this, "Actual bid", + action.getActualBid(), + String.valueOf(action.getActualBid())); // 2 + finish(); + } + + @Override + PossibleAction processInput() { + log.debug("Action was {}", action); + try { + int minBid = Integer.parseInt(((JTextField)inputElements.get(0)).getText()); + action.setMinimumBid(minBid); + } catch (NumberFormatException e) { + } + try { + int bidIncr = Integer.parseInt(((JTextField)inputElements.get(1)).getText()); + action.setBidIncrement(bidIncr); + } catch (NumberFormatException e) { + } + try { + int actualBid = Integer.parseInt(((JTextField)inputElements.get(2)).getText()); + action.setActualBid(actualBid); + } catch (NumberFormatException e) { + } + + log.debug("Action is {}", action); + return action; + + } + } + + private class BuyTrainDialog extends EditDialog { private static final long serialVersionUID = 1L; private BuyTrain action; diff --git a/src/main/java/net/sf/rails/ui/swing/hexmap/GUIHex.java b/src/main/java/net/sf/rails/ui/swing/hexmap/GUIHex.java index 324ac108c..f6da01e9e 100644 --- a/src/main/java/net/sf/rails/ui/swing/hexmap/GUIHex.java +++ b/src/main/java/net/sf/rails/ui/swing/hexmap/GUIHex.java @@ -17,6 +17,7 @@ import java.util.Map; import java.util.Set; +import net.sf.rails.util.Util; import rails.game.action.LayBaseToken; import rails.game.action.LayBonusToken; import net.sf.rails.algorithms.RevenueBonusTemplate; @@ -37,6 +38,7 @@ import net.sf.rails.ui.swing.GUIToken; import com.google.common.collect.Lists; +import rails.game.action.PossibleORAction; /** @@ -238,8 +240,8 @@ private GeneralPath makePolygon() { private static final int MARKS_DIRTY_MARGIN = 4; // positions of offStation Tokens - private static final int[] offStationTokenX = new int[] { -11, 0 }; - private static final int[] offStationTokenY = new int[] { -19, 0 }; + private static final int[] offStationTokenX = new int[] { 11, -11 }; + private static final int[] offStationTokenY = new int[] { -19, 19 }; // static fields private final HexMap hexMap; @@ -625,21 +627,40 @@ private void paintStationTokens(Graphics2D g2) { } } - // FIXME: Where to paint more than one offStationTokens? private void paintOffStationTokens(Graphics2D g2) { + Util.breakIf(hex.getId(), "M6"); int i = 0; for (BonusToken token : hex.getBonusTokens()) { HexPoint origin = dimensions.center.translate(offStationTokenX[i], offStationTokenY[i]); drawBonusToken(g2, token, origin); if (++i > 1) return; - } // check for temporary token if (upgrade instanceof TokenHexUpgrade && ((TokenHexUpgrade) upgrade).getAction() instanceof LayBonusToken) { HexPoint origin = dimensions.center.translate(offStationTokenX[i], offStationTokenY[i]); BonusToken token = ((LayBonusToken)((TokenHexUpgrade) upgrade).getAction()).getToken(); drawBonusToken(g2, token, origin); + if (++i > 1) return; + } + + // For 18VA: also check for off-station base tokens + for (BaseToken token : hex.getOffStationBaseTokens()) { + HexPoint origin = dimensions.center.translate(offStationTokenX[i], offStationTokenY[i]); + drawBaseToken(g2, token.getParent(), origin, dimensions.tokenDiameter); + if (++i > 1) return; } + // check for temporary token + if (upgrade instanceof TokenHexUpgrade) { + TokenHexUpgrade tokenUpgrade = (TokenHexUpgrade) upgrade; + PossibleORAction action = ((TokenHexUpgrade) upgrade).getAction(); + if (action instanceof LayBaseToken + && ((LayBaseToken)action).getType() == LayBaseToken.NON_CITY) { + HexPoint origin = dimensions.center.translate(offStationTokenX[i], offStationTokenY[i]); + PublicCompany company = tokenUpgrade.getAction().getCompany(); + drawBaseToken(g2, company, origin, dimensions.tokenDiameter); + } + } + } private void drawBaseToken(Graphics2D g2, PublicCompany co, HexPoint center, double diameter) { diff --git a/src/main/java/rails/game/action/BidStartItem.java b/src/main/java/rails/game/action/BidStartItem.java index 2b679b681..28072913a 100644 --- a/src/main/java/rails/game/action/BidStartItem.java +++ b/src/main/java/rails/game/action/BidStartItem.java @@ -70,6 +70,14 @@ public boolean isSelectForAuction() { return selectForAuction; } + public void setMinimumBid(int minimumBid) { + this.minimumBid = minimumBid; + } + + public void setBidIncrement(int bidIncrement) { + this.bidIncrement = bidIncrement; + } + public void setActualBid(int actualBid) { this.actualBid = actualBid; } diff --git a/src/main/java/rails/game/action/DiscardTrain.java b/src/main/java/rails/game/action/DiscardTrain.java index a6d632833..d1e1fd361 100644 --- a/src/main/java/rails/game/action/DiscardTrain.java +++ b/src/main/java/rails/game/action/DiscardTrain.java @@ -191,7 +191,7 @@ protected boolean equalsAs(PossibleAction pa, boolean asOption) { // See the top Javadoc. if (idsChanged) { executedAction.fixIds(this); - log.info("+++ Action corrected to {}", executedAction); + log.info("Action corrected to {}", executedAction); } return options; diff --git a/src/main/resources/LocalisedText.properties b/src/main/resources/LocalisedText.properties index 0346694f1..bef4df7f8 100644 --- a/src/main/resources/LocalisedText.properties +++ b/src/main/resources/LocalisedText.properties @@ -366,6 +366,7 @@ DIRECT_INCOME=Direct Rev. DiscardsBaseToken={0} discards a {1} base token on {2} DiscardingTrain=discarding {0}-train discount=discount of {0} +DivideEarnings=Split: {0} for company treasury, {1} for dividend DoesNotExist=Item does not exist DoesNotForm={0} does not form DoesNotHaveTheShares=Does not have the shares diff --git a/src/main/resources/data/18VA/CompanyManager.xml b/src/main/resources/data/18VA/CompanyManager.xml index 05f3bc159..862cb1b9e 100644 --- a/src/main/resources/data/18VA/CompanyManager.xml +++ b/src/main/resources/data/18VA/CompanyManager.xml @@ -1,14 +1,17 @@ - + - + + + + @@ -22,17 +25,52 @@ - + + + + + + + + + + longname="Tredegar Iron Works"> + + + + + + + + + + + + + + + + - - + + - + @@ -82,7 +82,7 @@ - + @@ -96,7 +96,7 @@ - + @@ -121,7 +121,7 @@ - + diff --git a/src/main/resources/data/18VA/Map.xml b/src/main/resources/data/18VA/Map.xml index 16c9b3649..313fd5485 100644 --- a/src/main/resources/data/18VA/Map.xml +++ b/src/main/resources/data/18VA/Map.xml @@ -29,7 +29,7 @@ - + @@ -53,7 +53,7 @@ - + diff --git a/src/main/resources/data/18VA/TileSet.xml b/src/main/resources/data/18VA/TileSet.xml index a6f9b2e7c..f6ea8d16c 100644 --- a/src/main/resources/data/18VA/TileSet.xml +++ b/src/main/resources/data/18VA/TileSet.xml @@ -60,16 +60,16 @@ - + - + - + - + @@ -83,9 +83,9 @@ - - - + + + diff --git a/src/main/resources/data/GamesList.xml b/src/main/resources/data/GamesList.xml index 07d4ef29d..c424fcf6b 100644 --- a/src/main/resources/data/GamesList.xml +++ b/src/main/resources/data/GamesList.xml @@ -281,6 +281,29 @@ Game variants: + + Playable (alpha) + 18VA + (c) David G.D. Hecht 2001-2005 + Rules version 1.00 of 10 May 2005 + + Not yet fully developed or tested: + - Share selling in emergency train buying + (taking loans, bonds and president cash works) + - Bankruptcy + - Passenger trains running through but ignoring + towns (mines). + + Notes: + - When the port and neighbouring city values are doubled, + the extra revenue of the city is added to the port value + in the revenue details display (the blue small-print text). + The reason is, that the doubling only occurs if the port is reached. + + + + + Not yet playable 1870 - Railroading across the Trans Mississippi @@ -327,12 +350,6 @@ Known Issues: - - Prototype - 18VA - - - Prototype - Not Playable 1862 diff --git a/src/test/resources/data/real/18EUK41.rails b/src/test/resources/data/real/18EUK41.rails new file mode 100644 index 0000000000000000000000000000000000000000..06ed48ca218f2a0c9c805a0d89a67814ea834349 GIT binary patch literal 41157 zcmeHQeT-aJb$|0_CjS12P4h*J41qK#o5i!;?PB4vmdHbg1@F z?a;y6;YY?MA2~GE?F75uQNLavZPr^?M`zout8aYno6mpc5AOTdFesf4s@LnywP?K) z>~$B-udQ5;I=}nL_aE5t!oNKi2EqDjz^`q+pi-MWbpeRpN!PFS8qLw!M(fFFdAi=c zHdkLA|NQ)K>}`Jje}MYMU`KsvDe895c9x?K@VMQzF;`c&y!eGL-SPH+EC=CKu)W!? zFHhB%dhO0ku&sBk6Lqh(o6GB~zxF7x-+5yzeC~z6?Y&?ROL_LnLbJXRb-KNvG~Nq# zpKGu6qGLDeou~zjd%?Ep#?@<0`0L4+&8zKBqjznk7i?dMIxCHCw{bn{1^doL*Q3^2 zbfVr|T5HyOjdrUSY*}2pdNt}o^PPI5h0G4wQ{(#7?hCDEW2MoHmd`hu z&@Kq)+aSbz8<3f6F9Vyf1t;!Wj8^NNdM{e6!&S$6i}jUgq0?A`@5>w5Z1;TByV36S zu1zJ1c}Ayn$7o2)0JeXa$G@pZ!nFqD|g_&hdt`m)`+&-nhQH*0Eo(*O)J! z|NF;3y0`oKruB#lc*E)THIS|{*LV_S-?G??RKgGmMpZEzw$K1RPB2IY%E6|{;#qaCZ{r-svZ{twwa75rB4!1`(lrNZOaz~^n? zU`KxfFF3Bk-*O3Ue{YiU{93aaV;YiDl@;(c(5JnMkk^~D^~+I{8R7fuky#0k?!yD{ z&V7+?=(P<29xlZfK)z)rctyS2Z7(&z z{g)T7K~E*mp9!j~*aMU?F9x@Fqn^9uY^%9(G1$|UHs{-mQExW53p&DTy|pnPMay6# z*8rt_U^du6W?fvZLoDhAzr;qx$n}Or=wcv51Wu3F{&L(1cph#FpWYOd6EZ@MIQ*zIa0^m9Hh(G6Wl%pzzgg-AQn-F)a(Y z7m_2k&bErJlLfDP6SQr3Y~!eqbKzxHx|WeHj*48?HPpIV3CC1P;@s9svToY0>$Xl; z)mBv70su^=*v!KnIVi7>vsPa?X0AmIy~HuZFHXT3eEGg4dhSGDI-Dg}8v8h$JCVy7 z;84iPsIB8|+xmtjOdM|or>HG_Q>~q3VJ#N3woub9D|}(BDNSS{e&wJbMs!elyy_k{>W z2P$L=N9{(AzVa9WU0>;;yYCU_DB}6&Y2ucUS#mTC}4_TEN3kq9qCG^zfx1c z*1gwZtF9D(*H^ww6P#EB^7Dmfd{nGrhxiNA>$ZENqS$;l#*NIZ+~IqZ(u#y^hc1|+}N^fHoi-b$p+TEx%9j!|> z5>7VTOGFIMQEdq7XnT=tW7*M#D!eMDL5cf~v%wwyMO-*VwW$<0D^k-bs77~DwV7qV z8f^-$P`-moU38+|Y_D}tl2Y(Qumw+1lkkZdD9OGi37IX)o;}C)(fGPCzn^)}BuD`TPhNXL)@5K3t~1e}AV+oyR2Ped_=2jtd4U|$he?j| z*r8gcB-nTxA+{YFmVkOEIFJMis-Gtt*Bh|PfRh`1fq!kC3GRi$HI(B|aBcN`C#rYX zIvZw@WG2|fUz`7-RJ&5&xX_I{Ku)f?cNLa!qFyEm{#q{I*@>Q7YjmRJ6R?y7KHK31 znPY$^JL40<-F1XES~hLmSEOf%RoqfJ9X~8B#9k#><^cDZ`|G#gfBjp3^>MqZL9NN< zDOgG03=-A>Kfo#~zAF3y?_iwAjFTMBHx0_VBi-hA8gfo#AUnl2FOBynyO+t9ZjVD@ z$>KyH_A0HRF#F+L5$$<22JH{Wh~PgU<{6{?WPdSZ+A~#@0~aOYFCaUIWbxYr{prTk zPtqMLtiI$O+W4GefcoP`NNxP`W3@xJYmW7oG==~ZMjb~DWhW>gF#Yh52qft~;0QCP zOb3vBN+ZCT7a)KO8u)VLK+eTUBj@e`^=GoE|HDi+PWOVP`?~&gV;+#Ci*=sZD^q_- zGPVBV0Y_p)@E=&fovqc#1Hy?xyMQy)2}4*1{qh;(TH(ae0TD8HV=R@vyo6of%2OvT%ivXaMoBD za6|Tu6vE*-8(s!QAW65l3#6t3oE(BtYBmv!nd-#&kS^e||7X$wIA!S;=v*-uNYcH< zRM3tCuxxDvEeiqw%k0r10dN8PIiY6{fbMBacld$8(!Irj!1V!}4eCA+&RI(hIuJNl zeZSE=M)$0xJE#XF=@thdwF@`^Y%-|R;qc8iTo0NIIzd002Ec+HVF&eqBwbvGjlD9H zK_7tl1Tmy|?9aLEuqe~dDXUJ=Iad)bc4h|~#rSn`P%cz;aNyi$+Pe?vc4w_I3k397 z+)2_E&~4x>3_1zw;)9(k#k_9DCW?bj6_RwNdpyt&QErP@@xV-ix~#+IN!CI%Sw25( zAWPB}2g7Yw_{v1o&t~F9S?$ISjuT8Xkd&b>msB_$L7Rxj>^5!T7~XD{t~l7O+Rf(6 z1_1b=Ii8I_gLVaHI7_%Vq`u)i53_k~OwuWtG7O5TlhhKHY^iUj?IWp8K((ad;t`W9 z3@@mqJ3OS+7bMQ>zTje*O-lR1(FyKJ)?1ISZP1Q!qdRQa-Gh6dVVk$9dv3eZScl_w zw|6nvf2G}7ik6|x%oW%&PrKVM2K&_6`8J)L3HD(%70b9iET5ilA6tgK();D)Tzv!1 z;8z!eSGcol-JUrOLeK`>1t`#`Ef_#)KU!b91{=j;b33Tjy3lGowFXsq$ndeWzca!O z@^i53zJ8TAHO~fHuXNfgdRYOk@ESf5+(Re3u%G*sp>HWuQ5VYfTXCgA$habJ(j?BI zf`FWqr{VX(j`XohwQ(%nkGa~e*jY#HWR9#Spy5e7{TkFulXNBb9&WI9vDD_WWloc}Hof>eSn4~L$4i`(EnNoAvS&QiKshp)-M9|VVa0E`uB-p_LjnV8nH*f;`A5_fw zX13=P2^%(#l^kusu*@8&wSRVk2URSg<-9A6rO``xJI*NWvp35ubjDJK8!I4O;JONI zeW$B<8~iM+q+EwvGw^ni>GnzlU*VksdbMv4uM5sKTJ4VXz1d(N+=|iebkDcvqRv(7 zl!OWdfIIC?7#=~253op=axaLM+ldUfsPLA3x-o!)K$w2t1;oV@u$RXz>naIIH{zn349VF)F+rdu#~%?e;CKU2Zm( zZ0A4>TsPKfN+<2c9&&PS+lcTc1$@E!DD)~=t%RF+mUxMMHrTUNZ=F+Tdcj_`HjcMs z!Q%M773ef@i;9s(b*sB==Q&@8Ze^ED)dlV|9)S&0X?t$S zw2d1UpiOe`$)GLXobPDZL)p;)fW2`?(=cc^1C>f@3XAS!{;0zjA?KmWbbMd~lFR!Nrs;7V?cDdEo)To4V8dn40%iP7*6fqr|7mS$2XN_JD zWHDKGl*G_;A5bwkP4jU0Hh23*ikuEkeUl8)?puRNg;V6T6{(n<(^k&xElz0hJhR(G zOtSGY!yzTne}qb>F@L%feV{t6}g#?XfoeYygT=FJm@ z#+MZZI>zWzhD09T4_`V&5i_s7Y99P0{==UAj4e^1yvl1tU4wy&hpij8-|T{Chj*;Gt3!uubMN@%6w7Z(BEZM;YNRA0n|pynd1H5aGLGd>r*M;Bk2JvY&|G{P;Y6&-Vb!clLW zQ;BicM@1eZBxM4Qrp<92Nf@+f`vp*&IC>;2L?Hz;XyMUKd^7Vo&jjf>SiU!HPr@<* zzH+hvk8mpLT1yuoEE8sd@1W%j&s1~v#r9QFVu)5U$@W>x|9wV)93^>RO46|SAO$+c z1hTRKH8(KS0-JronO!8)DETMJ7hDfWO<<-mfkEqD9oiM7pJZs^Nf#ay5`E^vMS31z4dkpE*8Qt-LgeD+7l{CPpSl%zQmQ z^<_AjulS4zYWY^qES!4FKt=k}5e!iyCa4fkA+LFg356%FZqOpg{9vlWEERBs-M_A9 z2yx~Zeh-;QloXC|5*1bEpp_7$!@wk)2ry98o*D=kxQ`wtRZldhPSxu9_IeVPB4N09_|Hq z=)Oi!2(fZ%kqoKd;4JM(ri?JNz$>L8 zWY5bGx|~s-<7$8SF;s-Dg=kG9C*S~u5wM=-@i4b zmf~FoQhd(h^lVX_Bm$X1fW}2R>DG9H;GIBH;}rhD;Uo<-PV8|YeE35vjrkQ<1U}70*zrp>h+8E!a}uIj6^ArFWfNZ(XsV&geVh|VGgp>! zwBgR-@c6Ah~mj$`&k$MJ~MOM%7E(0+>;0(q>^~ESjBLyQApbI)JT%x*hdCft@?U9)ZxSYLU zt6eNc{BCf2tA3v;ct-H>#>v!qm*rckSU`Sc1jzx!Bg_hAB`cBAK%5${}Jq-Upw3(GexTp;Buzp0x0@iq?_^(He{{`=ZR zI*NAKBwNV=K4GxTaaeCXj?)d0{Ci>sb4*1Jvh4&HQzo2gMJ{97_`0RGw+qS1i~xP7 zO&GyyP-ftR$2iIgTNxIv&2vGpG5lA(+-cvy#W$Nyu@f*`P)P%qkj3@%&-Dk+Bdj$S zZt0rafyAX&;1%|szH*aeU@{lN{!{*QQVrieDMz28qx7tZctL-}o7_PsbjaKR@;laL zhfRypVNoeG3Y$9UNl|q&^9c_FK;U`>F+;AOO+moB4A9}K3*htt;{&en+;1`4>Xp)Y z;F&(ljqXytvwU#jA=;Ub5@DpAfV>KiKR!%%a&asM_HH&!)r^?=pxdyKO|Z!sYY_Z_&M+)QdwnuiIkQ2zz5_*Vf_e zjzAsK>m8ht$~Z;C0?J1-S4dJ!>DsG!VP(|uk=E>jSxGLgGjGSd2RE!2NOH4EaUvQQ zk3G@i^;$0LMd;M*(Z=w3Qv{aGk6ogVu5BwlOy zmuR^Klr547d5hXYBRLC8CXz4+aD{FH=G<6g&kWI?_y*HBeC3zOLS~$yV_-0E9=?^g z^|26w-bO9qCi9N(81{WfNder_2p>01;e(F>jg+jr0zc_hKEiX@Bg0_j9Q*r*W2|Oj z9%h*{&o6|OzYajWplkCw0L3Qixs-+{L@IsV^ff7LlKVipWlL~&5eP5q7p=JmIXEsM7p4yfUJvpvg9~JZsPJgz z>6?SvMWC-&h$@%*pk}*VIB)$bRy{IxQdls^+?kHT6@<+7E!y{r4Ugdfk1SR7$h!+4cp(7YLK)6A3&5jBxEuid z%4<@sAovE7;mRvaQ3~u{B>Y%>YD#VuAK`6s5wA{9;WJ_Y$LyzT7IGjj#n5p{25c*U zl-f#n6-6sSBQ0v9x$w4;1+J`B-t77ox6eYxW)k#Eax4l%tQ)k)EeASHzsidd0;Gh*J78JSX5FT!BYmsif0# zR3LnE%p;t%(hf;VyigA0L@!XA+%WlIk)<|ba@H_ zBwIZzFQ(He2+$OE2yh|0E%z3DsIN$Zs)kzh^`M48i6!wgd8CP^Bl>!D&=_8C#-T@*7BVy5td+h`gFtkDLlyS=qooe^Zu^@HZ&=2!CxnjB1Y zoTehJ&RVO9mf%}w;YJ>u#b-YaWk4;xa((sH%&qjiRp_)8Bm0RD+(}avVndpQ$xw2+ z3L;qHnP6=^bBp+Z8hIf@+l{3`uCHfVLi*WQI4-@dsG6y+$uWf0JZ>oI*kEpG1~1Ki z2*OZmO7PNZ@<~*)#Fzs zSqLd?h)g+wo_mK7HrCB9%371hiurn)=YCxW9urF=L2_f`EgGGkqJUPN&$T&S2LABq z=Gfc?3mdSi0x)!B`b{0MeNI7sK(z}6E<3n?(I{CeFt~e!wartbSz>sje#Xj!#{6V# zjAfZi5kqM%1Bmwage;}cSk3L@3i^7x5Z1BG5q`BXYs|d%N5iU_k0=>X$G!vY^G1x} z)N$TwhBon4T0Aku|GfK62GqxMDs8ibEaBtEX5sxsmGm7m9-)|<7t6E=Hejm-?C_9v zoJ}7Dbv)!B@xtCQ^eo_MJqv(;WESu=&jS2G@m&ZiaD?F(4p7U~F_A8V5m1bAFSdhM zUymy|CfYbj!p!m-Z=ZDhw=t|gjc?D$(X(>&J9MO{J~D76jg8m7mlY~~$eOxvS|@~c zbo5WsG$eII)Um_;l;K2<51_ckwf9Z_0xF6m-k)o7x=f`(rAHZ{flbacIl+T`3`s4+ zAJ638l|GY`CK^}i+j#B&y~sEmQK!XJuMeZkd4Wq1(AP&6N= zCYuG4kSkc|;kYKsF-Uw;WYx$C#-0MG=bxn`CrvbRauVs-m%ovr`d)`ztN}HWyYs6r zyv*1epMJ(rd(tEeTj(xW{>4{pM4La%%a7IfTbM9p47e9v)UyYW zyW5{aEgyjiblz;OxSoDu`!#0gN-LX@*v2bJG4C|Ln_QmAzLlC%+u|A(a?clynoZ-L zvOBSFHkIn097@q8?6clnWd+ZEiOOXC%149hC;?o1XF?RooWn!LmHO%^gmo9>5e>C5 zsi<{K_{6jS#HGLqc#`twxTc>q5nJX2EYWbeiSkDVHm!`=T5X4?Nl%s9khP!2=n56x z%v;ok@?J4Ox@B#m{1qEqKV*a&*s}47nbzBx+oYj!z0G6NVM@AL=H^J0U>iyoy|oEX zVrYtwlJ1cXnYZN+Wl0Ga+L8%bWS9(1>2uSDNjO3Q-dq-=UGoD2wI z?V5ZNBY_#MVy+1f8{t)!+@(BhT|C}qjglvAj450hNma=CgBh391~P~-vpdc>bTiMq)P+9j`jxM>OXJN2?y!_t+&wtpfU2nA3!u77V$J{i# zwsIxxKK8K>-o5h!&%876y!BO&kL`W0TAw_50f4@g?qBOSTf;NW_9Nla=|=C`*~aSF zXJ_BAr}fz%1N4jD&c@R*5UC2#zviWf|IJ6fH_(o|!y z-|0?!+xyqLVeeX}wY0wa>fa>vcih+p-}~Y3mcDl@Q+e*{T&uAWc6)u#AA{DD>-}!y zY_r|zPIbCtt9>t+?ex9f=R0ft@aT<3H*5nveQ*2e=JK@`{Pp|ZuA{BhW;^JKbjg+DhNsF&B1Mn!R50I)LL2V|ItV=K(`|Ej-?6Ev~g1{br}#_qNWj zEiVI_zIVrbxZ3D8`r&*7@E+~YH&(*AZgUZURW^{2-Lql;MyK1qcDlK=6hdF%Y~2IZ zO)Pd+hL<~?O#<0-_TJHe~wtBmHuiR zrQkbC_bW*N)&Q`A?pLPUOX2#AcS}zbHVXvzy}g!X=KI|yI3FO`A4f3P0XqP(N8nci zuX53=0aqb#v~!gVU18e6zYxuvz2EkR*H?X{3eR5w-{2rYee7p&z>gaIE%~VVyW*H< z*IKOz)_}OGu7K?WL!DKWd80McxDvJ)6Mh~LQ>PlSst@*)s%4=;@1EdieW zy1sYJT&UoM`^<%8O|NZ;^nFQ4hBSoVDfnH{ZTDoQ=JL-F2beNRg%C^M0#5wJ{)#bg&oTYPyBe5zltBCBVVs*~Z zfFSXJ)p{DM2CGB3!E7#&W(7NSJpNXOE!Jol(3f{H? z*tP)*UL|nqD_C}6 zOWrW6QLL-m)^i0VcxzxZ6A5y11b`SUYZoC99P5J6O)lb>P5k)L75xnNjwE(pe72vL!B7+P5;(U`Rz8PJ)H zD?bs;PL$tBn*5T)6m{U8h~=%Ip-nij^mQG$WWX|1f-j}=Vx*O-S#u#8X{hVl)0i~q z%eqEXlv{!N$uvzE^D{(}m_(CAhFTADuM~Xr<1ZEqVHy;WCTuR8H9M@FBMs-bA<|!Mwl~)KfYhMZ+vP=%4ojn;kuM`pt03iBw*M^6Gx~; z>_M>2B?A|#o>oBu%9an#crS5p;_4G}O680hp_-bYp4UUxrkC7$o~l|w2?VMg;qgwZ zv(_a_p1tI4#U|iEBN@|-1ow^e}oz$fYenNDPkcpR9a%? zfEVcM33!S+U|^dbSzU$Fu0P3Uw^h}LcTs(K58MX@sVy5}tJS#yMK7*8Q~7(k-4B<; z?%uC|_>X`1=sPBX3Md+J1sqXVplh(sK>r;vG8uU3TME1R0y<7H9l>}yA=vkM64(pS z)d%2R@9wyqpn!CudA$i`T1?0B0Q1^5?cD{%KPYJ)=knA-H*ECQx*Mu6Jnh}Wm(_2m zcC9ovF7(1KfRlUfT7^1#*iQw)m*w_d-SEw8&2G4K9P0UCvRy7h9|d8uEnf2OY@lSr zCDq4VB0is^LfAKx#fJl16?AM|4>jZ{t!)bY$}^r$JDcQ-m6e z0$Y6zje|Itm5lQH8?(|hJmtCz!0kE ze33ACLPwVZ!%M^m;&6)(1Bn9Ai6AW8DNRK(wwc45^Z}Eybrf7Xx&&t?6x_0za2#%N z0VJk^MgTe&j1YQuvld_o-AHnQ85LaohfBIZ9B%P}Fi`-;1rk%i*QmKb>4DIg>f4n7 z%onDZ8t&#}fQDN<3c4;3ogoIS3Yu}rU?bCanSWl?`b?X0Bn~zr4w)K8LgGnnXPb@) z0#^vd-8^`trUkAzsBmEnOwZvW4l$!bSLysE4p$6U$KF&zUBe~4Nm^&?&E&~?0w!2; z#UTLgJW&imW;BrzVnGMUvVklPSJJtMSZ(Qga}BJBIP`Qxu#^@@5iB~75TWZ#B~`Vq z(2HP+CBRcD66<;|ixa=N!157t=)TaHe6r423sXz!zi%TzR+&Ic@3(lSowg`?5>bDu)-$Q#xn2hoAI_? z?RHk|vT#4v@sjs4YV5(L#*+%ZhD=qtZvYsqGz%D0r8C~9RicDSU!)Z(B>8nnCr2*V zvGO`XK%;6-cDR#TLjn|_Ol^7sWjsy;7GKB>K(5C{OT50e@^YL;8A3H& zA#`)0LE^j}DP=&C9G!H6vd$HU0JIfn0EP*Q3r^_i)OSi|JaM=K8{u?rDh7u0zK1s( z(#;5`;T9(}afFjbTWW7BLhg5}_@Cpj{ zTgEFsB6v}Jh_^aFd;C~AhP@xdQzdmkJu*}uA5zO6gj>)6?rzcp2;>gzg3S;&&EmoS z2P=Z`u)!j)V$XQDE;icdt(LyG$69{I=SE=tdG89i7wn{0PHj2ho!ZS78sLC>S<<>; zFFZmKy}%yonx+qo>@yD(9qo1-8;E`V(WmeJgTHP3VFed=t37yng&tq3-oWBbHM$;r ztkY@1b0&YkzxzA?{GlJcK+i0x_25-BME|Ge1M&ME{CEbQtwH4sfg=n}%@3V8Z?YkA z8>QqQ8k?r|O#eqn1=C_+n=`^v$KTIoU=b&=v9Ns=$8r~9`ECXl*IJ3S&#)_f86fWj6Dzd$^#~_+ zqtIhF3QW6E?zHn5ZbW724HA+qp~CP;&0_>$BIPy~W(SR93&W%hs&ra9TZ#?=HWro- zV!2mgkyM8!L0PMA+xv8^rR6SZ)=_ac5gPiJb=4BHa6V2)_p zorwn;^=C@UG!7#{rhr_@WW>mXbrSE7Lx4W$9CcEigA@a#I>$QO)-^(l{fgAYw!um za^o3A8j+OEfHW!wh%BQLJ(?JEg8AaC+IEz^xRO9tBC>ew$9W7NV1@qOI?SdD!3qe; zwkzVWZ8whhi`sVIrO;>CuJb^Gt$x8~i!>fhH8Kw``M@L&aN~#7L_WUdH11at3 zLcS^S)TqP*YYg-a#hpHMmh)cbmTJSl;tRj<(b^sA51lo5ywCszR(=9YJb2i)Z+2ad zAD&375X-PnsAyZXJx~Qt6d$O9_c0BgC`}K-xwe@dnrz!-w^Righ0jTe$F`Y_qm}ff0Fykh$6+K4?8w8PfIZAz=JLf;sZlI=H-$pzKreK{r96F57>6I2 zCL-40eW_49vK>R(Clw3>4vEowKZ4&YYdeXHu<%r-JPWDt!MYW^m`goUsn&+ola{a$ zo3NV*pX|ifznT7(FzxmCb}md1lEmH3?76e0SHrXoDee;ngA z|Hzn=r6j&dp%STk9yl0N{_#Cy*+Q5dMBK*ckgB)MX{C{nPOD--@bxrfN7Y5fRAzv~ zGF+?NV*tw80X$=a_yQTkAxtPMyA1 zA&U)e8=|{kvLLNtG@B?a$^|-Gl zfbp^jYDfEw8d>vRQ69BFikP9?Qv@?3L9NlylqvBXrt$Ju@B^~OBB(RuZ3;!jTD$^rK+#_Dw_AAkC_-~86yZ#@NrCv7f6xm6B;hrjt)(Y!|ACp_?Vu6kB93ANAROQbcSw%q6ggcnh|I? ze*`|}@oXM%Ux1ESd#Mq$bibqurD%|1G4lEj_~WV?*k6H4CyPz&-=ro93l&cyH7qvi zAacgZ5AEHYEwOjA?;U5M-L$x>C}ATlp-(w9NlSvQNDGPHTn#d<0xJ+JkH!k{Xi$bk z*YO9WP$o?HkF#2NS8TW3thHNCdS*+`l!jqkNwPB$whc#kP9W?5hrFC&y~5)O>2`q3 zv)79Eo76`I05og(LmhDKh^<08i1;Zq(!vOg(HRQ{($m(an2LZi)?doVoH-Z^&EXMo z=p!Ydr+APMmA}y7u*q~8HZ5h!opIiOi3arqrpLR9NobBTt6G!rP3nPfG1yOVr$*daM3anbG-GX#-91AsNS*x(VPyT((chzGAeCq(~{Evf&7Du`lGD#;VGfVj~OW z^;4;YIaX|kqyb_{i!hre+cBBV`+x3GB~oAou)L&EEgGg&xX38!17q!TdIp}lF9E^k zPC9ws&Eo}vrMeb&Jcwm%vIx?4bhoRny7?1IR=#$#jMA+WdTkSw@e09EV)zj_;dBW` zizHK)E=rOq1u#t)m22ru5m*_SOs(w{5}IS?_Dr2U3?=zEsmk-;rp>89EzAMTtg&^9 z>uPWaJ~lcP+u(!b=^~UF&{6CAz^d^3c?zXod?QvN`LI!OS|+#{JP_>|m2(?h7m4Zh zfZSqEVg@LU$RZh4?uiuKQy6Zf~|Byo$L6|LSu5{%ZED_;_N^a*nFvjG~F zxE14K@PG`}W3AIVw?Wu=y{1T?8>5)u`uYTN#5NV{&U^3_2+Tf}ta;h~5Y2|Hbz0XU zNlYd_>e_E)Ow;dy9B{}cT)DHkkacc@8=hAF>`yRZS!H)8&SJ3fSpQTfMG68}qElL_lT~$+M1p5W z@CdyvjbEEOqIOpg9GMN>vP#WrjFTeFj4bd^1>h+Oxkyuxja4gr!{MaIih>Wy2K z2L>uP3A3EIJF%Ncc}mRcDGUm6v!ZdXN@nFu;%rY7Oxekl*a{j~wW0#u*9Xe9IK4ev zk?10MoovX{G-YIwPJj~_X~qHx#@uCorpp@igfLr1;-r#DMI1`M^DuqTE9p>91d^1h zdMOftcvy%L=N~X;%W}(bvBWuEa%0p-Buz5NAg7#C@9Y_vkFY45R0i$xOoAw!+h7Fk z8tMv)BGE}q2+~=PF#sS2$`iP9RhfVty%dJyGu2I5fPo!x63p zPA)yt#fBryCd0{Cr;wgDIN@2qnVLh&^B~g2h9f)+oU&qaB8pv#70Ly8M0PjNbEFVk zpm>*7&@Ibzn(dpVr<9dykqGO6WhaW^vTPcWxy*_#Z3PQP(LpX8>5@StQ!W^@oQumv`j0NK zw=eZ^+Oqsab6Y#NVs)Wtbt3*{#iUFgGjc0t(os2Af~7G~vcfN_m&3O#ZRPFbmn23P zs-v1etXCG&>WV|r=qD-^7P__dW+zIc@*!ndE`d423153 zo-ECy0JcqDJXq3ilPH17>sLBfQy+!#DKb}&0@&tJ3?z9r zpkIGjdBgvpt!h#v_xBGJsf*ZVEv-9nu>?FFVu?}4_QsCeVEOlC2>cF(<43KsliqYcB$opkQRmRAhQUa^& zHw^JP%s+2cds6q$-A^xQ#`um~?^mD%Jdzq zj0g~;C0LVO=QdaX5z~K6k(nFL6Qi0qydDd^s<4g3oKZn-P)Vob@Qjra#kr0VCj+%m zo56KbEfIN3D~SOW@%m|YyhtAB^*<*V@E9n*Zu?h!oYyP%^3YC(h%(Z5u{b@rl1Z{{ zKrV9*%faxS{tp>Tx(Hk+?;rE*c%A+`wZNcK?I^_uNRXhgUifUH?b1su#CQGS3h>J0t zjJ;?WL~wp5b3ER@n6eku)O1o1Co&-BNrM5(A{UNyu>~Q+oxmaLQNg&WFsUdcU2Hfa zo#Jq$3&UYk7iR53m{2aj`K=~MBqfY0!R3q5QDZpffMVY z*72<2xCBd_g)?Sk>9-Igo}5gWc00F$8%q*WU>AcEfNjpTaR#$G#3&m9V!$@(5qTtP zYM@*j)4Zy=UQXcJxed71g;0M1`@xaxk_-cHWe(DlPTQ=fumc=b{(?^shTeWrOMN#J z=G!Gqz!{1YGqPHTNsz)saIc~%m-R$b*CChRDyKk2bWZ;tjJ!8^{~7{h9`Q<$B^z+oJUMkYDN~_hPmB5jyvPZiuY0&J< zGBYdfLXk>B75pH?u3%gVkKj1)hev*-po$790!1oN6iLA5VM8SdYz$6{M*@|^@PnLl z?!D)n?zuhPvn&4yMOW=i&+WeF@tt$;J@?$(y&wBgQ0@flb`K2??Y+Gd?AX0)_wGA( z?Z2b4|JKULog64RIM*X z%dKFW+%&zka5iea{bTRBY4bx@-WdkL@?v0)jh&!W89j6wh`x~SU+UEALsPZJ#c2LW zwSD1ub#eH!({I>T|LiY;`k7#Jb#5+dw@`OxzE4g{;w8;a3a`L zZ&v3gs&k!YYckl_xzLK*7n=3?<;BPpmVcY?w& zlpb2{w5rEzjb>}2*&1H#1mSeE6KpxzT~Mo1LWxh8;{h^U#P=hrxOeus4vzU z(W&Oer~#M422|KM-2`2ZH|HZ;u!U$IZbeb!RILuB<>}~B)B@6{8}-^ktrN{-Io3yp z10=_uQ>|(ZX%=>|4E6KPR;_bkp%ZMHiCPP_cDr^7$e9Ymy2AF8z@f1ejaTb)OZ94} z)@*cw^|MRo&jXuIaQ$qwSZ!51(QFm?9_Y+g7owR~Z4QVPS5S~G(^2Pgv(>q9q&7bv zL0!;nIRMk`oog-(oo_bJ*P|ggU1&Dot6q=hIzy8S3rn5q*?KfpYj=KkHfq(X_1c@G zc>r^PYycl%*?WTxQ9W7!qQGazy;Eq{o#-t(79`QajE4l7|*j89{lHfKDe#@ z^1Ab?5_r_{YU?6?)h>eUrK8OYAZvN7+G?$Uur~vQ_4d$t&~6A^13(xWtIf}Ls;$ms zCvrj+!k`R312-&H>u`NC7&r@`jky*{IT>tPs5NQ}OAB!AOt7sD($953sRK(y@=UP7 z6h-skrZ4Gc$@~t`{T2$~8Uj}^{=#HqK3bj%u4y~Lra|CNuw6kj+iBIH^#OxhyBW+h z!4UxW82w@x6wd@@&?*9rHZPK=TTYwIFJ$wk;P-=}<;4O@h3B`y_d0O8eZPVO&dcyO zUqH*>*v)u)sa}sU4N0le0{A}Y(_BQztM#es*{E(9;pa|dR=~5D;|cih!Dy~dtq2fq z!>xD##FT`bFK!?KRu(i!~PBmwv&Qx$cG=s%zV`Vyu=D|i%0i_(83O18jXBVr` zn{|TUGDgL|Z|5vDFc2aFm&beWh#LWmDn{_&3|v}VBm@Ry=nX*2tn5kH3TyU;nT>bQiFnhd*+Hpdc0y>lvXE+*G01N{P>w4ZzbX9_dLIu1Z3Ur^&)17Zls1fJ9K( zqH;*EW|gaH<(RJ}7#9ZowXoYGGC?4+AP3{JR9)o33r9V=#3iKr@wVkW5+-joM@k4wERA_FV;}jBeRnL6CN{Z4P`SnlvKN|MWuwU0-G{} z&{u^5mnvl2Smz^MndW9>_6DPBm#)kN#>K)&npuTwLU7@nxWL?#_GX5<BT9K`V zuDUZxlk{hrB^2#kivE082>Pe?iEB|a0Sq(L`U`!f)hbjaAl{UsDl>s#HmnS%*;?2p zBo~zBLcE*11~0q?*n9hb=C3suZc20EWD4WG;RdkO{xs>Ost8GrBe*9nh**gWxkO9Q zR!Rt6Qy6cQ8<5%CQY^(v!1O1q>`K#hEh;5+Wscf5LcfPv`CDavsJt`@KbCVND)26g z-$bGVXTi51eyP2hSSW#pSUz!q+0z|n6pME@Td>CatX#N-K_ojKUa7_prXS@Qsux@$3K!U340wu#VJ*87b~!DzbWvO8}= zOh0E$`buNcR`8Catgwxh<8Xqi(AP*`uy5iu;IsUu8=w;|cu zywHWTo0<_r-t4Za;04K>O!fmcrTO#}OHD%Ulk>@hlFgdH92}VH{c|n_Wug=Z zyEn~p9q{%$fWmNc_u?X?84LT3tFO1Y$^pt%Zi72vvbk<0s@I#BApv7Dpp=T8Y;>aY zQEU769{JG!{NX!BA*%)n43mvT_C=@~E*qj>2cM8s3N@r}TNz6Bn`Ak%hh!;28aP!N zr2QQN@CsBd0Ebp^Q#Wpq(KuMURD--B29J1vyKR^ZZiHkMq}awyzT{LZshR( z8C+v7+ux9WS*Wg@ZbvO3$M@X02#MXOlS;x|=GzBa(VLcPt!RE663pPjEt3^J0Eimj zyf=7J72%EMZ5_EpdOks!nJ3dR#CRok6d-vC9(DY0?|<2)fB%~gyIdvSrV=RP8(u4c zPwb9S#UJ1>6UIn|J-tnYcKU9*{eNUhgKYKy(ua*Nh9jfdw4brGf6$GKqmA)W>?kau zRL??L9w>Mez_zm|MhJg^yvJ##d6A3?Q6O$-DsYpnDbkK?w!kqkgf@b))kpj^5=WqanN2$ehMzVL*Ri8N&)^sUJ!5Z- z5dMIroT*C13jDKdfn#6@Z3N>6OFZUw$H?u#MMm~z6OW2+NFYt_+3XUY`%4_R0 zb_!K{2E?bCcn+j{py7z;_Y6^7d4{+Ppt0YI={h@VCkL-mJ8{Hg?c`7ehZBUl-!4=H zN56B#^N%M|U!ft92Q-PwE|2}flOsmp!nJ)i6rqg*;{r?k0FI{|?OU>G$6UitJHKm4 zeFcUfgkZSUHr;C!U(2Iw4rh*a8$uk>IBucP#KV)eODgn>hM)EtT|8r}eOe0Y`J zhR{YZUT?wB4JABiU8C>O)lWNrA1d{gYe-juV7L$zXIAEb(y4u`Y$rk+!Khk_vFX@R zOFLNGfZ}N92Sa32U`YIeV7MqW=E}!c>ls2D!MJF_h|xabX!i?-pLTvQq<$(GE((oV z@3pqe@4E*Th6u)z4~!W%a`!u&_-W&cY3#@ygi>$A-D+qM@=OU`x`C(46HcwP*q@08 zhGvYZf+waVqf$c7utwnq0vsd0JUF% zr9aTLSzz2Rgh#5tQs3Kzkf?XAb;@;sYjbl)o1O2KM&5LR#X`#Yc!b#U6*Phu5_OXeXH$~}a?teK9r|f=)UY~R#)-O^-J%R%G!J!sR=*QxKk@t!OML~g z;EoWxxLU^|#R;Cm^eeHSIJ;kF1DEYx$45E>U)#`?Z-2Ti|SiJx}d7rK4Q34#C!>;d-fqSbPX$gqdo>}sXGvv#4R z<=dJ(wzM)1i-k2n0yf+V1y~KbW}&)_yQ={!FkDmvV3ospIzM z!YyX=ZPY@VQ^CL-e4TF80K3U~Qo^$yHx+C+*J>^>K(wHf*n({3qP{bM)Wze!aNmhovSuZ zs*+ByO)U-MLj|xlynO+@8n#(m5!tNJUYvKIs)FCUB|>$>cGzAYzd*VTB|V}5O?ihr zZE~R1s;(gS^1=_J@vmR9&|8#5X55E5X zU;LOJXt4{li|7!AA0pSn4fydmJkCK5HyFn|={Vm*dl{?JN@C&r_}g$Nl1w-FHlBh# zy>xRr2Cm&2#{dC$#2tgg?ZY!r9P&TbiT$q+He6YIG;6~Y^LsSc)gH}mu6LzqHwR_R z_Ij2_-^ny}A=6A9k$9TP(Zw1^B+ZGX#}l3(W^!r;H>eQ}R_Eyfnxz$b&6Eqam)hC5X{-K4_FU_1BsxK@9v zd0-wMw%Ngj$Ezz)f|t$&*GcJ8yJL$%2zr8J1~!NIHmkt4l4yDE0z78~Pg{Xnjl{q) z5Yf|L@Pt?Oym=}nIdZ_3mQGj2dxIOOuni9c9kTQtWojVb1TwfwhC3qJQQ$X=!PR`u z;TJ6)6kZDRsNMJN!tElvrR50;6=AZsk-6OrI2K`^b`j=JOg+!m6Lj7ln@=h3kci#sKHgIpa zJ_F{s)NbM%n?@evwV(WH7oXUU_!pJjbdKAHiV&c{=N{u6d(x$jha;#?6R% z(wb>isjq@#?g=#~E+kHFCd>YlnKP04%727C+SttjwI#yw?EDw+wL*`HIlZ4g3!T|h zOMNAD+uWq4_`?XhMBV7#q_jdqIWQL*X$>@7NDk~W^qz)^sJw{UWreuC zSQ@6#Q2roJf-pR8<)B7I^rjd>$V)1}ecbCBR!}fJg;2@UWkp^Q%9=~o?GFD zhXp|+UdWR<91*dTpgYhy#=?F&JuPeo46^X5Y3xaQiO8Y6MEXC3;Yp*C`h;HNC~o>8 zC)s5xb`S1IB|HEh2@k=N_~cPHR6p-e^bN3|PEUY`li&!A^adM{TS#L=?Vht(}fo}2WRkDI3&cjmQ68}GlzIxc(bZgS;(26d4b5P*-JMqH|wUB?n zV0u?_tQ8{4l+xKDWp4;~(Cy#rw>|l3v+reU?XQoDx`VnvqxIt;e|RL>T?q-*B-y9a z8%ou3c*J$Gm@dI1cSGqn?pk)7dc<93jPN#oF4Q`4AvJy%Gj)y3sX9shRF84-l1v)W zkG9#7a9`>cRdmLlBOzN(E5wDQkmLQBgYIEtqo7Bqk;owF1e6e0+DfBAy}aZ85x3bl zb*`DIEOXJyav>?}SpUK)`-!~D3LVu35`}45Y78w`m8CWWxuv1dBzS&;T-RnDDSyv4 zawyPh0Os$*>Dm6W_l4I(KMAV_z19pUcgj$$LJ6*`mEdN@N?`Be79sjw|A3j*#>((u zWn^&A;E1v?bFr*R7Y20|x^P`hFlG?a2bX8aWx_{cB_W=_o}QJ-7}qr967jFe;-UBQfz{?0Esp;KI(u#>|fk z*R}G|B8LG>7CDNv$bqGWAIb+!5}(8V8^GTLYbRra)3bvIPo{%T#0}_qh!=Ju@lU4^ zXX9zVVQ13Wf7a1DQ=d8(EVXB2^AYk-x4t{k?|P@OZYprz$PlGdf;^T*5n z`YbA(kitgVPow|k%N}y$(!z2f3A^8N$&y2MZMTc09N)*7LqCA_I0tcQ#_!S@f4T>kPH8K6{bjdLAq->);w|ig;h8cbgv4% z@uCEGv4{@f%M(~xdefl(%FswCsa;(8lKL@x^{wxJWc3>@C7m$rqd27gT)wvDf|J z*_+;S7`kk;rHF2ZkK%v?vx0##xpYEUjJ@Y?{U46-H{wD838|m(I38 zT_DeEqB%2}EDjHEPP|Ned)4Y~Z_@|8MDP+CMsIH-EMBN1d4}6|0lwLk>8O~IzLN_z zP4)?WCl~5|PO1a&JfxQ#fG8X%w?P1H`thyS$NemtT^(%Ah#1o zwvLu*>nLmzqHk!lu&s0xpSL|5N2w{tH;M}Wu&xLKZL)ueNbx|r>m@=12vl&VpVP>G z)1w`S@kuvGBBei5&yAJXU4hE-k{A>NwxM#1EgrWo>LBN3N7I8E7otlQj~H1BcRNcy zVhXY|nL=PEWQt_TK336@k;ze*89n8J3%hi0xP+JVQssERMx^@&QNQ3BY_)Kam;3EsDGtO2!ZItvS^*ygpw^u4DEKX7M-{X!{BrZC;C$9 z<)l(x6P568(T1fk+GH+s72gWWGxRvi3_O}3bdnthC0b)XOlqQvw9rv}SJy%ZdZ`fP zx>_*C9*HFgKHLeejjzstYY08As_C)zOpn*|P|>v~mMOZ!EDd%Bzs|UcVL#Y~+k`R$ zBd*QB&f_t|L%+cdQAyxtx>vzXMNZss>?#qpMD~6U!o3}xYifhBG)!)l_Rp_}xs#>Q z%4JC7Yj#}4XthQ$Om(JdmvIsyD|G^3$@l@Gs@YlwF-!Vp+^osk6+KzIVka`)DfHso z4D$lLJsr*qTdXvhK{|Vt1}s^an4hA?AFvG2QOp5~y4h*^Dgt6z#HU7zSMcMzeD*y$ z(-{YL32Y69AD;ZsGh^}G3K$%w6*NxLywIU9TZu{ zZL3H*vn3gvn4iXp3&~hk2yQb$jy1RhaouKwxN0*}h@!vCs$0}*^DI9jXPMPxJUL6l z5IC0xVm*jw5s+nL(cpw~mb8F?As;+VwFm4|pr@&NknrWCp|*J zqr6;5j}g19wB1Rk%)zNuUX^SEAWaS3f^R4 zqALR&C%ouR;5#j_iKefcpt?8X$1V7AE1$iD&U)k}jmpw_;Qd2JRiDau`8LpdFQA6! zuRxW1(4ZF?IOH(GGuY$I52(x-+-3wf5rl9{>~_PcF-?!pzzIzPOGEzPK0zlSsKvd( z*C>aD0>usx3UU-?*|t|YG{c$4wKI=v_*sV39 z2}^*qeJ?$_V;pyA}^qwd<8QHhj)a;IU@Sj$dUXLH+_;C-P{T`p4 zp)-qC_cK7Hv&IQ{_%3|IdPSyliVn!a(&-Jb0fzw26%5*}pOq*!LeW#(0ABhGsb6qt z#N>WPyAl_Y8`OCXQ<1tGOJde{e97YC!lgV& znm#IEL&)E^nYdj#Tde&j$P-TE-7?Wqf8d|8yQwo7j9SO zUEFc#Hpztq(VtoS@Y=Qgo!usdjshT2n6`+3Pxx$tP}sc|%a@pwafav{QtXasFS6M? zEGc@v!AFOym{xD-u%VcaN%z~4#~+MxB~6JnW=ga)2g=!*iBBf3q$#n&2}nphwh+TVWi!T*204xih%FT;X&w_-jPNEHh$_R9+U0_BkqOSE9smP{eKe`cmhA3 zlExjld zVb*$x<3chb{)Z?y3O^TdOi88@g^p5IEURN|os=w^e`JF;ce*U53cb(__-X)y&F-OZcDqEJfzJ_C67cnD-wpfhU?Iu~LGR=(Nl z<`Rj4TawCDR_GWPBYcyEedJWRV=G%}{i9Pqp4Z)3mFYJ)93oT6=o9O?pU^la`=jhb8US8J40jJl~dG!)HN-r6m;-hX%GrPdvGoHxkKwqUh zQ24Bw!iQX=b&uO*`mxYa`irr_uAJc48wCx^G4toX*&#kb9=bS?z$1v>gKD#mt7rt7(PPUejKRYVPTlwBb$FqZV(_%FF|_O8Jr^EUqb?g#tILI? zu6@Ul431yUtFF*d>Wb6rAEF5A^8H3NY0G=_>MCj}UB%v@ z6c509x){>`T%Z9s_35NlDa)+2VY!eDYj^e+KIJ->+|xjzW0ds`z-bw&hXGMq94*7! z+kNx-1d=8z&K#YrXqOKoDf@@MR;GnQ!#GxfDse`l3@?)LG3F{dmx4&q+7{9vI--p7RaQNut1_?)OiwCRTr`trZATniJi!L zNA1;>Cean-%I}EDpc&i2cP*jEyLIRRKREPww+%f!(IaL5A$p`|C%|RGUGRZNW|YC^ zuv3)0IU9~1d*rdO(fuiYyc<6r;|Ekv{5t%<6WJ)i&H<3bM5o#gxINcZg3HVuLLG*I^oD z)AKE8wEXIN7U;^KL`9Iy`#mNXIq?p<6%2f*1)=bvydVf21%mc^e+Y?8Kkk;udq*1i zK`=IF=Gx|5NPyVDLU!q{Erb+D08i$y%0dZ1f~DQV{NZToyL~CF>Q`B2t(E0MQuZH> zDJ5S1R9@Q(9b;QxSY`H#i5J9zXv>Ho2oK?#+_N&%eYkvLiQZa`nzl z0I(}*j$8=F)|=%*?p6-wMO{d#7KN7V-qNI9%ypqKB}?COl97XWN(Tb)g9Cx5(g)0c zGkO(2YlSI3npaWLNhvDY>j5*SsPvwzR`jP@QTV}%{!}X}vY;FNhgI}zPSG#rRaEFG zMWu124~s?f56*K;$9{qm{uDp{lh6K(&wfT{R<(F5)=n%U;r;(>)C*r`_2mt&nPGs? zQTj@oj1P=@Nhdsr{~3= z=N80rzv?n`tu7amx;RUPgBR=a>`P3Ht~z=bgX;k>rmWqS-E*3PUYB246Y>aaG~*7` z3S@wMEUJfpw4Oy17|*Q`^4w@GUdSB-2pwbbc!Scoh;PJ(MR9r5_&IQSlzqTZONo5* VHhr{|8bq+l2rC literal 0 HcmV?d00001 diff --git a/src/test/resources/data/real/18EUU43.report b/src/test/resources/data/real/18EUU43.report new file mode 100644 index 000000000..fd7e13c6f --- /dev/null +++ b/src/test/resources/data/real/18EUU43.report @@ -0,0 +1,1790 @@ +GameIs,18EU +PlayerIs,1,Mark +PlayerIs,2,Mike +PlayerIs,3,John +PlayerIs,4,Barry +PlayerCash,350 +BankHas,10600 +StartOfPhase,2 +BankSizeIs,10600 +StartOfInitialRound,1 +HasPriority,Mark + +SelectForAuctioning,Mark,7 +DeclinedToBid,Mike +DeclinedToBid,John +DeclinedToBid,Barry +ITEM_PRICE_REDUCED,7,90 +BuysItemFor,Mark,7,90 +Floats,7 + +SelectForAuctioning,Mike,13 +DeclinedToBid,John +DeclinedToBid,Barry +BID_ITEM,Mark,100,13 +PASSES,Mike +PASSES,John +PASSES,Barry +BuysItemFor,Mark,13,100 +Floats,13 + +SelectForAuctioning,John,1 +BID_ITEM,Barry,100,1 +PASSES,Mark +PASSES,Mike +BID_ITEM,John,105,1 +PASSES,Barry +BuysItemFor,John,1,105 +Floats,1 + +SelectForAuctioning,Barry,2 +DeclinedToBid,Mark +BID_ITEM,Mike,100,2 +BID_ITEM,John,120,2 +PASSES,Barry +PASSES,Mark +PASSES,Mike +BuysItemFor,John,2,120 +Floats,2 + +SelectForAuctioning,Mark,8 +DeclinedToBid,Mike +DeclinedToBid,John +DeclinedToBid,Barry +ITEM_PRICE_REDUCED,8,90 +DeclinedToBid,Mark +DeclinedToBid,Mike +DeclinedToBid,John +DeclinedToBid,Barry +ITEM_PRICE_REDUCED,8,80 +BuysItemFor,Mark,8,80 +Floats,8 + +SelectForAuctioning,Mike,12 +DeclinedToBid,John +DeclinedToBid,Barry +DeclinedToBid,Mark +ITEM_PRICE_REDUCED,12,90 +DeclinedToBid,Mike +DeclinedToBid,John +DeclinedToBid,Barry +DeclinedToBid,Mark +ITEM_PRICE_REDUCED,12,80 +BuysItemFor,Mike,12,80 +Floats,12 + +SelectForAuctioning,John,5 +DeclinedToBid,Barry +DeclinedToBid,Mark +DeclinedToBid,Mike +ITEM_PRICE_REDUCED,5,90 +DeclinedToBid,John +DeclinedToBid,Barry +DeclinedToBid,Mark +DeclinedToBid,Mike +ITEM_PRICE_REDUCED,5,80 +DeclinedToBid,John +BuysItemFor,Barry,5,80 +Floats,5 + +SelectForAuctioning,Barry,3 +DeclinedToBid,Mark +DeclinedToBid,Mike +DeclinedToBid,John +ITEM_PRICE_REDUCED,3,90 +DeclinedToBid,Barry +DeclinedToBid,Mark +DeclinedToBid,Mike +DeclinedToBid,John +ITEM_PRICE_REDUCED,3,80 +BuysItemFor,Barry,3,80 +Floats,3 + +SelectForAuctioning,Mark,14 +DeclinedToBid,Mike +DeclinedToBid,John +BID_ITEM,Barry,100,14 +PASSES,Mark +PASSES,Mike +PASSES,John +BuysItemFor,Barry,14,100 +Floats,14 + +SelectForAuctioning,Mike,9 +DeclinedToBid,John +DeclinedToBid,Barry +DeclinedToBid,Mark +ITEM_PRICE_REDUCED,9,90 +DeclinedToBid,Mike +DeclinedToBid,John +DeclinedToBid,Barry +DeclinedToBid,Mark +ITEM_PRICE_REDUCED,9,80 +BuysItemFor,Mike,9,80 +Floats,9 + +SelectForAuctioning,John,10 +DeclinedToBid,Barry +DeclinedToBid,Mark +DeclinedToBid,Mike +ITEM_PRICE_REDUCED,10,90 +DeclinedToBid,John +DeclinedToBid,Barry +DeclinedToBid,Mark +DeclinedToBid,Mike +ITEM_PRICE_REDUCED,10,80 +DeclinedToBid,John +DeclinedToBid,Barry +DeclinedToBid,Mark +DeclinedToBid,Mike +ITEM_PRICE_REDUCED,10,70 +BuysItemFor,John,10,70 +Floats,10 + +SelectForAuctioning,Barry,15 +DeclinedToBid,Mark +DeclinedToBid,Mike +DeclinedToBid,John +ITEM_PRICE_REDUCED,15,90 +BuysItemFor,Barry,15,90 +Floats,15 + +SelectForAuctioning,Mark,4 +DeclinedToBid,Mike +DeclinedToBid,John +DeclinedToBid,Barry +ITEM_PRICE_REDUCED,4,90 +DeclinedToBid,Mark +DeclinedToBid,Mike +DeclinedToBid,John +DeclinedToBid,Barry +ITEM_PRICE_REDUCED,4,80 +BuysItemFor,Mark,4,80 +Floats,4 + +SelectForAuctioning,Mike,6 +DeclinedToBid,John +DeclinedToBid,Barry +DeclinedToBid,Mark +ITEM_PRICE_REDUCED,6,90 +DeclinedToBid,Mike +DeclinedToBid,John +DeclinedToBid,Barry +DeclinedToBid,Mark +ITEM_PRICE_REDUCED,6,80 +DeclinedToBid,Mike +DeclinedToBid,John +DeclinedToBid,Barry +DeclinedToBid,Mark +ITEM_PRICE_REDUCED,6,70 +DeclinedToBid,Mike +DeclinedToBid,John +DeclinedToBid,Barry +DeclinedToBid,Mark +ITEM_PRICE_REDUCED,6,60 +DeclinedToBid,Mike +DeclinedToBid,John +DeclinedToBid,Barry +DeclinedToBid,Mark +ITEM_PRICE_REDUCED,6,50 +BuysItemFor,Mike,6,50 +Floats,6 + +SelectForAuctioning,John,11 +DeclinedToBid,Barry +DeclinedToBid,Mark +DeclinedToBid,Mike +ITEM_PRICE_REDUCED,11,90 +DeclinedToBid,John +DeclinedToBid,Barry +DeclinedToBid,Mark +DeclinedToBid,Mike +ITEM_PRICE_REDUCED,11,80 +DeclinedToBid,John +DeclinedToBid,Barry +DeclinedToBid,Mark +DeclinedToBid,Mike +ITEM_PRICE_REDUCED,11,70 +DeclinedToBid,John +DeclinedToBid,Barry +DeclinedToBid,Mark +DeclinedToBid,Mike +ITEM_PRICE_REDUCED,11,60 +DeclinedToBid,John +DeclinedToBid,Barry +DeclinedToBid,Mark +BuysItemFor,Mike,11,60 +NewTrainAvailable,2,3 +Floats,11 +Has,1,0 +Has,2,0 +Has,3,0 +Has,4,0 +Has,5,0 +Has,6,0 +Has,7,0 +Has,8,0 +Has,9,0 +Has,10,0 +Has,11,0 +Has,12,0 +Has,13,0 +Has,14,0 +Has,15,0 +Has,Mark,0 +Has,Mike,80 +Has,John,55 +Has,Barry,0 +START_OR,0.1 + +CompanyOperates,1,John +LaysTileAt,1,9,I2,SW +LaysTileAt,1,202,H3,NE +CompanyRevenue,1,70 +CompanySplits,1,70 +RECEIVES,1,35 +Payout,John,35,1,100 + +CompanyOperates,2,John +LaysTileAt,2,3,F3,S +LaysTileAt,2,58,G2,NW +CompanyRevenue,2,90 +CompanySplits,2,90 +RECEIVES,2,45 +Payout,John,45,1,100 + +CompanyOperates,3,Barry +LaysTileAt,3,8,K2,S +LaysTileAt,3,4,M2,S +CompanyRevenue,3,50 +CompanySplits,3,50 +RECEIVES,3,25 +Payout,Barry,25,1,100 + +CompanyOperates,4,Mark +LaysTileAt,4,202,G10,NW +LaysTileAt,4,4,H9,SW +CompanyRevenue,4,40 +CompanySplits,4,40 +RECEIVES,4,20 +Payout,Mark,20,1,100 + +CompanyOperates,5,Barry +LaysTileAt,5,201,S8,SW +LaysTileAt,5,8,U8,SW +CompanyRevenue,5,60 +CompanySplits,5,60 +RECEIVES,5,30 +Payout,Barry,30,1,100 + +CompanyOperates,6,Mike +LaysTileAt,6,58,L11,S +LaysTileAt,6,57,K10,NW +CompanyRevenue,6,60 +CompanySplits,6,60 +RECEIVES,6,30 +Payout,Mike,30,1,100 + +CompanyOperates,7,Mark +LaysTileAt,7,9,F9,SW +LaysTileAt,7,58,G8,NW +CompanyRevenue,7,40 +CompanySplits,7,40 +RECEIVES,7,20 +Payout,Mark,20,1,100 + +CompanyOperates,8,Mark +LaysTileAt,8,202,P13,S +LaysTileAt,8,9,N13,S +CompanyRevenue,8,60 +CompanySplits,8,60 +RECEIVES,8,30 +Payout,Mark,30,1,100 + +CompanyOperates,9,Mike +LaysTileAt,9,58,D11,SE +LaysTileAt,9,4,E12,NW +CompanyRevenue,9,50 +CompanySplits,9,50 +RECEIVES,9,25 +Payout,Mike,25,1,100 + +CompanyOperates,10,John +LaysTileAt,10,201,R5,S +LaysTileAt,10,57,T5,S +CompanyRevenue,10,60 +CompanySplits,10,60 +RECEIVES,10,30 +Payout,John,30,1,100 + +CompanyOperates,11,Mike +LaysTileAt,11,9,Q10,SW +LaysTileAt,11,57,R9,SW +CompanyRevenue,11,50 +CompanySplits,11,50 +RECEIVES,11,25 +Payout,Mike,25,1,100 + +CompanyOperates,12,Mike +LaysTileAt,12,202,C4,S +LaysTileAt,12,8,D5,NW +CompanyRevenue,12,40 +CompanySplits,12,40 +RECEIVES,12,20 +Payout,Mike,20,1,100 + +CompanyOperates,13,Mark +LaysTileAt,13,201,L7,N +LaysTileAt,13,58,J7,NE +CompanyRevenue,13,40 +CompanySplits,13,40 +RECEIVES,13,20 +Payout,Mark,20,1,100 + +CompanyOperates,14,Barry +LaysTileAt,14,202,M4,SW +LaysTileAt,14,58,O4,SW +CompanyRevenue,14,40 +CompanySplits,14,40 +RECEIVES,14,20 +Payout,Barry,20,1,100 + +CompanyOperates,15,Barry +LaysTileAt,15,201,Q2,NE +LaysTileAt,15,4,P3,SW +CompanyRevenue,15,80 +CompanySplits,15,80 +RECEIVES,15,40 +Payout,Barry,40,1,100 + +EndOfOperatingRound,0.1 +ORWorthIncrease,Mark,0.1,90 +ORWorthIncrease,Mike,0.1,100 +ORWorthIncrease,John,0.1,110 +ORWorthIncrease,Barry,0.1,115 +Has,1,35 +Has,2,45 +Has,3,25 +Has,4,20 +Has,5,30 +Has,6,30 +Has,7,20 +Has,8,30 +Has,9,25 +Has,10,30 +Has,11,25 +Has,12,20 +Has,13,20 +Has,14,20 +Has,15,40 +Has,Mark,90 +Has,Mike,180 +Has,John,165 +Has,Barry,115 +START_OR,0.2 + +CompanyOperates,1,John +CompanyRevenue,1,70 +CompanySplits,1,70 +RECEIVES,1,35 +Payout,John,35,1,100 + +CompanyOperates,2,John +CompanyRevenue,2,90 +CompanySplits,2,90 +RECEIVES,2,45 +Payout,John,45,1,100 + +CompanyOperates,3,Barry +LaysTileAt,3,9,O2,S +CompanyRevenue,3,80 +CompanySplits,3,80 +RECEIVES,3,40 +Payout,Barry,40,1,100 + +CompanyOperates,4,Mark +LaysTileAt,4,9,I8,SW +CompanyRevenue,4,80 +CompanySplits,4,80 +RECEIVES,4,40 +Payout,Mark,40,1,100 + +CompanyOperates,5,Barry +LaysTileAt,5,4,T7,SW +CompanyRevenue,5,70 +CompanySplits,5,70 +RECEIVES,5,35 +Payout,Barry,35,1,100 + +CompanyOperates,6,Mike +CompanyRevenue,6,60 +CompanySplits,6,60 +RECEIVES,6,30 +Payout,Mike,30,1,100 + +CompanyOperates,7,Mark +LaysTileAt,7,58,F7,SE +CompanyRevenue,7,50 +CompanySplits,7,50 +RECEIVES,7,25 +Payout,Mark,25,1,100 + +CompanyOperates,8,Mark +LaysTileAt,8,8,L13,S +CompanyRevenue,8,60 +CompanySplits,8,60 +RECEIVES,8,30 +Payout,Mark,30,1,100 + +CompanyOperates,9,Mike +LaysTileAt,9,8,F13,NW +CompanyRevenue,9,70 +CompanySplits,9,70 +RECEIVES,9,35 +Payout,Mike,35,1,100 + +CompanyOperates,10,John +LaysTileAt,10,8,S6,S +CompanyRevenue,10,60 +CompanySplits,10,60 +RECEIVES,10,30 +Payout,John,30,1,100 + +CompanyOperates,11,Mike +CompanyRevenue,11,50 +CompanySplits,11,50 +RECEIVES,11,25 +Payout,Mike,25,1,100 + +CompanyOperates,12,Mike +LaysTileAt,12,4,C6,SW +CompanyRevenue,12,80 +CompanySplits,12,80 +RECEIVES,12,40 +Payout,Mike,40,1,100 + +CompanyOperates,13,Mark +LaysTileAt,13,58,K6,N +CompanyRevenue,13,90 +CompanySplits,13,90 +RECEIVES,13,45 +Payout,Mark,45,1,100 + +CompanyOperates,14,Barry +LaysTileAt,14,4,L5,SW +CompanyRevenue,14,90 +CompanySplits,14,90 +RECEIVES,14,45 +Payout,Barry,45,1,100 + +CompanyOperates,15,Barry +CompanyRevenue,15,100 +CompanySplits,15,100 +RECEIVES,15,50 +Payout,Barry,50,1,100 + +EndOfOperatingRound,0.2 +ORWorthIncrease,Mark,0.2,140 +ORWorthIncrease,Mike,0.2,130 +ORWorthIncrease,John,0.2,110 +ORWorthIncrease,Barry,0.2,170 +Has,1,70 +Has,2,90 +Has,3,65 +Has,4,60 +Has,5,65 +Has,6,60 +Has,7,45 +Has,8,60 +Has,9,60 +Has,10,60 +Has,11,50 +Has,12,60 +Has,13,65 +Has,14,65 +Has,15,90 +Has,Mark,230 +Has,Mike,310 +Has,John,275 +Has,Barry,285 +StartStockRound,1 +HasPriority,Mark +PASSES,Mark +START_COMPANY_LOG,Mike,KKÖB,100,200,2,20,KKÖB +MERGE_MINOR_LOG,Mike,11,KKÖB,50,1 +GetShareForMinor,Mike,10,KKÖB,11 +SharesPutInTreasury,70,KKÖB +PaysForTokens,KKÖB,100,5 +START_COMPANY_LOG,John,SNCF,90,180,2,20,SNCF +MERGE_MINOR_LOG,John,2,SNCF,90,1 +GetShareForMinor,John,10,SNCF,2 +SharesPutInTreasury,70,SNCF +PaysForTokens,SNCF,100,5 +BUY_SHARE_LOG,Barry,10,SNCF,SNCF,90 +PASSES,Mark +MERGE_MINOR_LOG,Mike,6,KKÖB,60,1 +GetShareForMinor,Mike,10,KKÖB,6 +ExchangesBaseToken,KKÖB,6,N11 +MinorCloses,6 +MERGE_MINOR_LOG,John,1,SNCF,70,1 +GetShareForMinor,John,10,SNCF,1 +ExchangesBaseToken,SNCF,1,J1 +MinorCloses,1 +Floats,SNCF +BUY_SHARE_LOG,Barry,10,KKÖB,KKÖB,100 +Floats,KKÖB +PASSES,Mark +BUY_SHARE_LOG,Mike,10,KKÖB,KKÖB,100 +BUY_SHARE_LOG,John,10,SNCF,SNCF,90 +PASSES,Barry +PASSES,Mark +PASSES,Mike +PASSES,John + +END_SR,1 +Has,3,65 +Has,4,60 +Has,5,65 +Has,7,45 +Has,8,60 +Has,9,60 +Has,10,60 +Has,12,60 +Has,13,65 +Has,14,65 +Has,15,90 +Has,KKÖB,410 +Has,SNCF,420 +Has,Mark,230 +Has,Mike,10 +Has,John,5 +Has,Barry,95 +START_OR,1.1 + +CompanyOperates,3,Barry +CompanyRevenue,3,80 +CompanySplits,3,80 +RECEIVES,3,40 +Payout,Barry,40,1,100 + +CompanyOperates,4,Mark +CompanyRevenue,4,80 +CompanySplits,4,80 +RECEIVES,4,40 +Payout,Mark,40,1,100 +BuysTrain,4,2,7,99 + +CompanyOperates,5,Barry +LaysTileAt,5,3,U6,N +CompanyRevenue,5,80 +CompanySplits,5,80 +RECEIVES,5,40 +Payout,Barry,40,1,100 + +CompanyOperates,7,Mark +LaysTileAt,7,8,G6,NE +CompanyDoesNotPayDividend,7 +BuysTrain,7,2,8,143 + +CompanyOperates,8,Mark +LaysTileAt,8,9,K12,NW +CompanyDoesNotPayDividend,8 +BuysTrain,8,3,IPO,200 +FirstTrainBought,3 +StartOfPhase,3 + +CompanyOperates,9,Mike +CompanyRevenue,9,70 +CompanySplits,9,70 +RECEIVES,9,35 +Payout,Mike,35,1,100 + +CompanyOperates,10,John +CompanyRevenue,10,80 +CompanySplits,10,80 +RECEIVES,10,40 +Payout,John,40,1,100 + +CompanyOperates,12,Mike +LaysTileAt,12,9,C8,NW +CompanyRevenue,12,80 +CompanySplits,12,80 +RECEIVES,12,40 +Payout,Mike,40,1,100 + +CompanyOperates,13,Mark +LaysTileAt,13,57,I6,S +CompanyRevenue,13,100 +CompanySplits,13,100 +RECEIVES,13,50 +Payout,Mark,50,1,100 +BuysTrain,13,2,4,60 + +CompanyOperates,14,Barry +CompanyRevenue,14,90 +CompanySplits,14,90 +RECEIVES,14,45 +Payout,Barry,45,1,100 +BuysTrain,14,2,15,110 + +CompanyOperates,15,Barry +CompanyDoesNotPayDividend,15 +BuysTrain,15,3,IPO,200 + +CompanyOperates,KKÖB,Mike +LaysTileAt,KKÖB,581,N11,SW +LAYS_FREE_TOKEN_ON,KKÖB,R9 +CompanyRevenue,KKÖB,150 +CompanyPaysOutFull,KKÖB,150 +Payout,Mike,75,5,10 +Payout,Barry,15,1,10 +Payout,KKÖB,60,4,10 +PRICE_MOVES_LOG,KKÖB,100,E3,110,F3 +BuysTrain,KKÖB,3,IPO,200 +BuysTrain,KKÖB,3,IPO,200 + +CompanyOperates,SNCF,John +LaysTileAt,SNCF,580,J1,S +CompanyRevenue,SNCF,180 +CompanyPaysOutFull,SNCF,180 +Payout,John,90,5,10 +Payout,Barry,18,1,10 +Payout,SNCF,72,4,10 +PRICE_MOVES_LOG,SNCF,90,E4,100,F4 +BuysTrain,SNCF,3,IPO,200 +NewTrainAvailable,3,4 + +EndOfOperatingRound,1.1 +ORWorthIncrease,Mark,1.1,90 +ORWorthIncrease,Mike,1.1,200 +ORWorthIncrease,John,1.1,180 +ORWorthIncrease,Barry,1.1,178 +Has,3,105 +Has,4,61 +Has,5,105 +Has,7,1 +Has,8,3 +Has,9,95 +Has,10,100 +Has,12,100 +Has,13,55 +Has,14,0 +Has,15,0 +Has,KKÖB,70 +Has,SNCF,292 +Has,Mark,320 +Has,Mike,160 +Has,John,135 +Has,Barry,253 +START_OR,1.2 + +CompanyOperates,3,Barry +CompanyRevenue,3,80 +CompanySplits,3,80 +RECEIVES,3,40 +Payout,Barry,40,1,100 + +CompanyOperates,4,Mark +LaysTileAtFor,4,8,H11,S,60 +CompanyRevenue,4,80 +CompanySplits,4,80 +RECEIVES,4,40 +Payout,Mark,40,1,100 + +CompanyOperates,5,Barry +CompanyRevenue,5,80 +CompanySplits,5,80 +RECEIVES,5,40 +Payout,Barry,40,1,100 + +CompanyOperates,7,Mark +CompanyRevenue,7,80 +CompanySplits,7,80 +RECEIVES,7,40 +Payout,Mark,40,1,100 + +CompanyOperates,8,Mark +LaysTileAt,8,8,J11,N +CompanyRevenue,8,90 +CompanySplits,8,90 +RECEIVES,8,45 +Payout,Mark,45,1,100 + +CompanyOperates,9,Mike +CompanyRevenue,9,70 +CompanySplits,9,70 +RECEIVES,9,35 +Payout,Mike,35,1,100 + +CompanyOperates,10,John +CompanyRevenue,10,80 +CompanySplits,10,80 +RECEIVES,10,40 +Payout,John,40,1,100 +BuysTrain,10,2,SNCF,139 + +CompanyOperates,12,Mike +LaysTileAt,12,8,D9,NW +CompanyRevenue,12,80 +CompanySplits,12,80 +RECEIVES,12,40 +Payout,Mike,40,1,100 +BuysTrain,12,3,KKÖB,1 + +CompanyOperates,13,Mark +CompanyRevenue,13,160 +CompanySplits,13,160 +RECEIVES,13,80 +Payout,Mark,80,1,100 + +CompanyOperates,14,Barry +CompanyRevenue,14,120 +CompanySplits,14,120 +RECEIVES,14,60 +Payout,Barry,60,1,100 + +CompanyOperates,15,Barry +CompanyRevenue,15,150 +CompanySplits,15,150 +RECEIVES,15,75 +Payout,Barry,75,1,100 + +CompanyOperates,KKÖB,Mike +LaysTileAt,KKÖB,577,S8,NE +CompanyRevenue,KKÖB,210 +CompanyPaysOutFull,KKÖB,210 +Payout,Mike,105,5,10 +Payout,Barry,21,1,10 +Payout,KKÖB,84,4,10 +PRICE_MOVES_LOG,KKÖB,110,F3,122,G3 + +CompanyOperates,SNCF,John +LaysTileAt,SNCF,9,H1,S +CompanyRevenue,SNCF,250 +CompanyPaysOutFull,SNCF,250 +Payout,Barry,25,1,10 +Payout,John,125,5,10 +Payout,SNCF,100,4,10 +PRICE_MOVES_LOG,SNCF,100,F4,110,G4 + +EndOfOperatingRound,1.2 +ORWorthIncrease,Mark,1.2,205 +ORWorthIncrease,Mike,1.2,240 +ORWorthIncrease,John,1.2,215 +ORWorthIncrease,Barry,1.2,283 +Has,3,145 +Has,4,41 +Has,5,145 +Has,7,41 +Has,8,48 +Has,9,130 +Has,10,1 +Has,12,139 +Has,13,135 +Has,14,60 +Has,15,75 +Has,KKÖB,155 +Has,SNCF,531 +Has,Mark,525 +Has,Mike,340 +Has,John,300 +Has,Barry,514 +StartStockRound,2 +HasPriority,Barry +BUY_SHARE_LOG,Barry,10,SNCF,SNCF,110 +BUY_SHARE_LOG,Mark,10,SNCF,SNCF,110 +START_COMPANY_LOG,Mike,NS,82,164,2,20,NS +MERGE_MINOR_LOG,Mike,12,NS,139,2 +GetShareForMinor,Mike,10,NS,12 +SharesPutInTreasury,70,NS +PaysForTokens,NS,100,5 +BUY_SHARE_LOG,John,10,SNCF,SNCF,110 +START_COMPANY_LOG,Barry,SNCB,100,200,2,20,SNCB +MERGE_MINOR_LOG,Barry,15,SNCB,75,1 +GetShareForMinor,Barry,10,SNCB,15 +SharesPutInTreasury,70,SNCB +PaysForTokens,SNCB,100,5 +BUY_SHARE_LOG,Mark,10,SNCF,SNCF,110 +BUY_SHARE_LOG,Mike,10,NS,NS,82 +PASSES,John +BUY_SHARE_LOG,Barry,10,SNCB,SNCB,100 +START_COMPANY_LOG,Mark,DR,100,200,2,20,DR +MERGE_MINOR_LOG,Mark,4,DR,41,1 +GetShareForMinor,Mark,10,DR,4 +SharesPutInTreasury,70,DR +PaysForTokens,DR,100,5 +BUY_SHARE_LOG,Mike,10,NS,NS,82 +Floats,NS +BUY_SHARE_LOG,John,10,SNCB,SNCB,100 +Floats,SNCB +MERGE_MINOR_LOG,Barry,3,SNCB,145,1 +GetShareForMinor,Barry,10,SNCB,3 +ExchangesBaseToken,SNCB,3,J1 +MinorCloses,3 +MERGE_MINOR_LOG,Mark,8,DR,48,1 +GetShareForMinor,Mark,10,DR,8 +ExchangesBaseToken,DR,8,P13 +MinorCloses,8 +PASSES,Mike +PASSES,John +MERGE_MINOR_LOG,Barry,14,SNCB,60,2 +GetShareForMinor,Barry,10,SNCB,14 +ExchangesBaseToken,SNCB,14,M4 +MinorCloses,14 +MERGE_MINOR_LOG,Mark,13,DR,135,2 +GetShareForMinor,Mark,10,DR,13 +ExchangesBaseToken,DR,13,L7 +MinorCloses,13 +Floats,DR +PASSES,Mike +PASSES,John +BUY_SHARE_LOG,Barry,10,DR,DR,100 +BUY_SHARE_LOG,Mark,10,SNCB,SNCB,100 +PASSES,Mike +PASSES,John +PASSES,Barry +PASSES,Mark + +END_SR,2 +SoldOut,SNCF +PRICE_MOVES_LOG,SNCF,110,G4,122,G3 +Has,5,145 +Has,7,41 +Has,9,130 +Has,10,1 +Has,SNCB,680 +Has,NS,367 +Has,KKÖB,155 +Has,SNCF,971 +Has,DR,424 +Has,Mark,5 +Has,Mike,12 +Has,John,90 +Has,Barry,4 +START_OR,2.1 + +CompanyOperates,5,Barry +CompanyRevenue,5,90 +CompanySplits,5,90 +RECEIVES,5,45 +Payout,Barry,45,1,100 +BuysTrain,5,2,SNCB,190 + +CompanyOperates,7,Mark +CompanyRevenue,7,80 +CompanySplits,7,80 +RECEIVES,7,40 +Payout,Mark,40,1,100 +BuysTrain,7,2,DR,80 + +CompanyOperates,9,Mike +CompanyRevenue,9,70 +CompanySplits,9,70 +RECEIVES,9,35 +Payout,Mike,35,1,100 +BuysTrain,9,2,KKÖB,165 + +CompanyOperates,10,John +CompanyRevenue,10,150 +CompanySplits,10,150 +RECEIVES,10,75 +Payout,John,75,1,100 + +CompanyOperates,KKÖB,Mike +LaysTileAt,KKÖB,15,K10,NW +LAYS_FREE_TOKEN_ON,KKÖB,K10 +CompanyRevenue,KKÖB,200 +CompanyPaysOutFull,KKÖB,200 +Payout,Barry,20,1,10 +Payout,Mike,100,5,10 +Payout,KKÖB,80,4,10 +PRICE_MOVES_LOG,KKÖB,122,G3,135,H3 +BuysTrain,KKÖB,2,NS,100 +BuysTrain,KKÖB,4,IPO,300 +FirstTrainBought,4 +StartOfPhase,4 +TrainsRusted,2 + +CompanyOperates,SNCF,John +LaysTileAt,SNCF,577,H3,SW +CompanyRevenue,SNCF,160 +CompanyPaysOutFull,SNCF,160 +Payout,Barry,32,2,10 +Payout,John,96,6,10 +Payout,Mark,32,2,10 +PRICE_MOVES_LOG,SNCF,122,G3,135,H3 +BuysTrain,SNCF,4,IPO,300 +BuysTrain,SNCF,4,IPO,300 + +CompanyOperates,SNCB,Barry +LaysTileAt,SNCB,144,K6,S +CompanyRevenue,SNCB,170 +CompanyPaysOutFull,SNCB,170 +Payout,Barry,102,6,10 +Payout,John,17,1,10 +Payout,Mark,17,1,10 +Payout,SNCB,34,2,10 +PRICE_MOVES_LOG,SNCB,100,E3,110,F3 +BuysTrain,SNCB,4,IPO,300 +NewTrainAvailable,4,5 + +CompanyOperates,DR,Mark +LaysTileAt,DR,581,E10,SW +LAYS_FREE_TOKEN_ON,DR,I6 +CompanyRevenue,DR,160 +CompanyPaysOutFull,DR,160 +Payout,Barry,16,1,10 +Payout,Mark,80,5,10 +Payout,DR,64,4,10 +PRICE_MOVES_LOG,DR,100,E3,110,F3 +BuysTrain,DR,5,IPO,500 +FirstTrainBought,5 +StartOfPhase,5 +CompanyDiscardsTrain,SNCF,3,Pool + +CompanyOperates,NS,Mike +LaysTileAt,NS,7,C10,S +CompanyRevenue,NS,150 +CompanyPaysOutFull,NS,150 +Payout,Mike,75,5,10 +Payout,NS,75,5,10 +PRICE_MOVES_LOG,NS,82,D4,90,E4 +BuysTrain,NS,5,IPO,500 + +EndOfOperatingRound,2.1 +ORWorthIncrease,Mark,2.1,255 +ORWorthIncrease,Mike,2.1,315 +ORWorthIncrease,John,2.1,276 +ORWorthIncrease,Barry,2.1,324 +Has,5,0 +Has,7,1 +Has,9,0 +Has,10,76 +Has,SNCB,604 +Has,NS,42 +Has,KKÖB,0 +Has,SNCF,371 +Has,DR,68 +Has,Mark,174 +Has,Mike,222 +Has,John,278 +Has,Barry,219 +START_OR,2.2 + +CompanyOperates,5,Barry +CompanyDoesNotPayDividend,5 + +CompanyOperates,7,Mark +CompanyDoesNotPayDividend,7 + +CompanyOperates,9,Mike +CompanyDoesNotPayDividend,9 + +CompanyOperates,10,John +CompanyDoesNotPayDividend,10 + +CompanyOperates,KKÖB,Mike +LaysTileAt,KKÖB,582,S8,NE +LAYS_FREE_TOKEN_ON,KKÖB,S8 +CompanyRevenue,KKÖB,270 +CompanyPaysOutFull,KKÖB,270 +Payout,Barry,27,1,10 +Payout,Mike,135,5,10 +Payout,KKÖB,108,4,10 +PRICE_MOVES_LOG,KKÖB,135,H3,150,I3 +SELL_SHARES_LOG,KKÖB,4,10,40,KKÖB,600 + +CompanyOperates,SNCF,John +LaysTileAt,SNCF,57,G4,SW +CompanyRevenue,SNCF,320 +CompanyPaysOutFull,SNCF,320 +Payout,Barry,64,2,10 +Payout,John,192,6,10 +Payout,Mark,64,2,10 +PRICE_MOVES_LOG,SNCF,135,H3,150,I3 + +CompanyOperates,SNCB,Barry +LaysTileAt,SNCB,83,K2,SW +CompanyRevenue,SNCB,230 +CompanyPaysOutFull,SNCB,230 +Payout,Barry,138,6,10 +Payout,John,23,1,10 +Payout,Mark,23,1,10 +Payout,SNCB,46,2,10 +PRICE_MOVES_LOG,SNCB,110,F3,122,G3 + +CompanyOperates,DR,Mark +LaysTileAt,DR,584,E10,S +LAYS_FREE_TOKEN_ON,DR,E10 +CompanyRevenue,DR,400 +CompanyPaysOutFull,DR,400 +Payout,Barry,40,1,10 +Payout,Mark,200,5,10 +Payout,DR,160,4,10 +PRICE_MOVES_LOG,DR,110,F3,122,G3 + +CompanyOperates,NS,Mike +LaysTileAt,NS,579,C4,SW +CompanyRevenue,NS,210 +CompanyPaysOutFull,NS,210 +Payout,Mike,105,5,10 +Payout,NS,105,5,10 +PRICE_MOVES_LOG,NS,90,E4,100,F4 + +EndOfOperatingRound,2.2 +ORWorthIncrease,Mark,2.2,389 +ORWorthIncrease,Mike,2.2,365 +ORWorthIncrease,John,2.2,317 +ORWorthIncrease,Barry,2.2,398 +Has,5,0 +Has,7,1 +Has,9,0 +Has,10,76 +Has,SNCB,650 +Has,NS,147 +Has,KKÖB,708 +Has,SNCF,371 +Has,DR,228 +Has,Mark,461 +Has,Mike,462 +Has,John,493 +Has,Barry,488 +StartFinalMinorExchangeRound +HasFirstTurn,Mark +MERGE_MINOR_LOG,Mark,7,NS,1,0 +GetShareForMinor,Mark,10,NS,7 +ExchangesBaseToken,NS,7,E10 +MinorCloses,7 +MERGE_MINOR_LOG,Mike,9,DR,0,0 +GetShareForMinor,Mike,10,DR,9 +NoBaseTokenExchange,DR,9,E10 +MinorCloses,9 +MERGE_MINOR_LOG,John,10,KKÖB,76,0 +GetShareForMinor,John,10,KKÖB,10 +NoBaseTokenExchange,KKÖB,10,R5 +MinorCloses,10 +MERGE_MINOR_LOG,Barry,5,KKÖB,0,0 +GetShareForMinor,Barry,10,KKÖB,5 +NoBaseTokenExchange,KKÖB,5,S8 +MinorCloses,5 + +END_SR,2 +Has,SNCB,650 +Has,NS,148 +Has,KKÖB,708 +Has,SNCF,371 +Has,DR,228 +Has,Mark,461 +Has,Mike,462 +Has,John,493 +Has,Barry,488 +StartStockRound,3 +HasPriority,Mike +SELL_SHARE_LOG,Mike,10,DR,122 +PRICE_MOVES_LOG,DR,122,G3,110,G4 +START_COMPANY_LOG,Mike,KPEV,100,200,2,20,KPEV +SelectedHomeBase,KPEV,Hex E10 (Berlin) +SharesPutInTreasury,80,KPEV +PaysForTokens,KPEV,100,5 +SELL_SHARE_LOG,John,10,SNCB,122 +PRICE_MOVES_LOG,SNCB,122,G3,110,G4 +START_COMPANY_LOG,John,KBS,100,200,2,20,KBS +SelectedHomeBase,KBS,Hex G4 (Cologne) +SharesPutInTreasury,80,KBS +PaysForTokens,KBS,100,5 +SELL_SHARES_LOG,Barry,2,10,20,SNCF,300 +START_COMPANY_LOG,Barry,FS,100,200,2,20,FS +SelectedHomeBase,FS,Hex F5 (Dortmund) +SharesPutInTreasury,80,FS +PaysForTokens,FS,100,5 +BUY_SHARE_LOG,Mark,10,DR,Pool,110 +BUY_SHARE_LOG,Mike,10,KPEV,KPEV,100 +BUY_SHARE_LOG,John,10,KBS,KBS,100 +BUY_SHARE_LOG,Barry,10,DR,DR,110 +BUY_SHARE_LOG,Mark,10,KKÖB,Pool,150 +BUY_SHARE_LOG,Mike,10,KPEV,KPEV,100 +BUY_SHARE_LOG,John,10,KBS,KBS,100 +BUY_SHARE_LOG,Barry,10,FS,FS,100 +BUY_SHARE_LOG,Mark,10,SNCB,Pool,110 +BUY_SHARE_LOG,Mike,10,KPEV,KPEV,100 +Floats,KPEV +MonetiseTreasuryShares,KPEV,500 +BUY_SHARE_LOG,John,10,KBS,KBS,100 +Floats,KBS +MonetiseTreasuryShares,KBS,500 +BUY_SHARE_LOG,Barry,10,FS,FS,100 +PASSES,Mark +PASSES,Mike +BUY_SHARE_LOG,John,10,DR,DR,110 +BUY_SHARE_LOG,Barry,10,FS,FS,100 +Floats,FS +MonetiseTreasuryShares,FS,500 +PASSES,Mark +PASSES,Mike +PASSES,John +BUY_SHARE_LOG,Barry,10,FS,Pool,100 +PASSES,Mark +PASSES,Mike +PASSES,John +PASSES,Barry + +END_SR,3 +Has,SNCB,650 +Has,NS,148 +Has,KBS,900 +Has,KPEV,900 +Has,KKÖB,708 +Has,FS,900 +Has,SNCF,371 +Has,DR,448 +Has,Mark,91 +Has,Mike,84 +Has,John,5 +Has,Barry,78 +START_OR,3.1 + +CompanyOperates,KKÖB,Mike +LaysTileAt,KKÖB,81,J11,S +CompanyRevenue,KKÖB,300 +CompanyPaysOutFull,KKÖB,300 +Payout,Barry,60,2,10 +Payout,Mike,150,5,10 +Payout,John,30,1,10 +Payout,Mark,30,1,10 +PRICE_MOVES_LOG,KKÖB,150,I3,165,J3 + +CompanyOperates,SNCF,John +LaysTileAt,SNCF,15,G4,SW +LAYS_FREE_TOKEN_ON,SNCF,G4 +CompanyRevenue,SNCF,330 +CompanyWithholds,SNCF,330 +PRICE_MOVES_LOG,SNCF,150,I3,135,H3 + +CompanyOperates,DR,Mark +LaysTileAt,DR,579,G10,SW +CompanyRevenue,DR,470 +CompanySplits,DR,470 +RECEIVES,DR,230 +Payout,Barry,48,2,10 +Payout,John,24,1,10 +Payout,Mark,144,6,10 +Payout,DR,24,1,10 +PRICE_MOVES_LOG,DR,110,G4,122,H4 +SELL_SHARES_LOG,DR,1,10,10,DR,122 + +CompanyOperates,SNCB,Barry +LaysTileAt,SNCB,15,I6,S +CompanyRevenue,SNCB,320 +CompanySplits,SNCB,320 +RECEIVES,SNCB,160 +Payout,Barry,96,6,10 +Payout,Mark,32,2,10 +Payout,SNCB,32,2,10 +PRICE_MOVES_LOG,SNCB,110,G4,122,H4 +SELL_SHARES_LOG,SNCB,1,10,10,SNCB,122 + +CompanyOperates,NS,Mike +LaysTileAt,NS,58,D3,NE +CompanyRevenue,NS,330 +CompanyPaysOutFull,NS,330 +Payout,Mike,165,5,10 +Payout,Mark,33,1,10 +Payout,NS,132,4,10 +PRICE_MOVES_LOG,NS,100,F4,110,G4 + +CompanyOperates,KPEV,Mike +LaysTileAt,KPEV,582,C4,N +LAYS_FREE_TOKEN_ON,KPEV,C4 +CompanyDoesNotPayDividend,KPEV +PRICE_MOVES_LOG,KPEV,100,E3,90,D3 +BuysTrain,KPEV,3,KKÖB,200 +BuysTrain,KPEV,5,IPO,500 +NewTrainAvailable,5,6 + +CompanyOperates,KBS,John +LaysTileAt,KBS,582,H3,SW +LAYS_FREE_TOKEN_ON,KBS,H3 +CompanyDoesNotPayDividend,KBS +PRICE_MOVES_LOG,KBS,100,E3,90,D3 +BuysTrain,KBS,4,SNCF,199 +BuysTrain,KBS,6,IPO,600 +FirstTrainBought,6 +StartOfPhase,6 +TrainsRusted,3 + +CompanyOperates,FS,Barry +LaysTileAt,FS,57,F5,SW +CompanyDoesNotPayDividend,FS +PRICE_MOVES_LOG,FS,100,E3,90,D3 +BuysTrain,FS,6,IPO,600 +NewTrainAvailable,6,8 +BuysTrain,FS,P,Pool,100 +BUY_SHARES_LOG,FS,2,10,20,FS,Pool,180 + +EndOfOperatingRound,3.1 +ORWorthIncrease,Mark,3.1,330 +ORWorthIncrease,Mike,3.1,390 +ORWorthIncrease,John,3.1,-59 +ORWorthIncrease,Barry,3.1,270 +Has,SNCB,964 +Has,NS,280 +Has,KBS,101 +Has,KPEV,200 +Has,KKÖB,908 +Has,FS,20 +Has,SNCF,900 +Has,DR,824 +Has,Mark,330 +Has,Mike,399 +Has,John,59 +Has,Barry,282 +START_OR,3.2 + +CompanyOperates,KKÖB,Mike +LaysTileAt,KKÖB,584,N11,S +DuplicateTokenRemoved,KKÖB,N11 +CompanyRevenue,KKÖB,190 +CompanyPaysOutFull,KKÖB,190 +Payout,Mike,95,5,10 +Payout,Barry,38,2,10 +Payout,Mark,19,1,10 +Payout,John,19,1,10 +PRICE_MOVES_LOG,KKÖB,165,J3,180,K3 +BuysTrain,KKÖB,8,IPO,800 +FirstTrainBought,8 +StartOfPhase,8 +TrainsRusted,4 +BuysTrain,KKÖB,P,Pool,100 + +CompanyOperates,SNCF,John +LaysTileAt,SNCF,143,F3,NW +CompanyDoesNotPayDividend,SNCF +PRICE_MOVES_LOG,SNCF,135,H3,122,G3 +BuysTrain,SNCF,8,IPO,800 +BuysTrain,SNCF,P,Pool,100 + +CompanyOperates,DR,Mark +LaysTileAt,DR,576,P13,SE +CompanyRevenue,DR,330 +CompanyPaysOutFull,DR,330 +Payout,Barry,66,2,10 +Payout,Mark,198,6,10 +Payout,John,33,1,10 +PRICE_MOVES_LOG,DR,122,H4,135,H3 +BuysTrain,DR,8,IPO,800 + +CompanyOperates,SNCB,Barry +LaysTileAt,SNCB,81,G6,SW +CompanyDoesNotPayDividend,SNCB +PRICE_MOVES_LOG,SNCB,122,H4,110,G4 +BuysTrain,SNCB,8,IPO,800 +BuysTrain,SNCB,P,Pool,100 + +CompanyOperates,NS,Mike +LaysTileAt,NS,147,F3,NE +CompanyRevenue,NS,390 +CompanyPaysOutFull,NS,390 +Payout,Mike,195,5,10 +Payout,Mark,39,1,10 +Payout,NS,156,4,10 +PRICE_MOVES_LOG,NS,110,G4,122,H4 +SELL_SHARES_LOG,NS,4,10,40,NS,488 + +CompanyOperates,KPEV,Mike +LaysTileAt,KPEV,582,G10,SW +CompanyRevenue,KPEV,390 +CompanyPaysOutFull,KPEV,390 +Payout,Mike,195,5,10 +PRICE_MOVES_LOG,KPEV,90,D3,100,E3 +BuysTrain,KPEV,P,Pool,100 + +CompanyOperates,KBS,John +LaysTileAt,KBS,583,J1,S +LAYS_FREE_TOKEN_ON,KBS,J1 +CompanyRevenue,KBS,280 +CompanyPaysOutFull,KBS,280 +Payout,John,140,5,10 +PRICE_MOVES_LOG,KBS,90,D3,100,E3 + +CompanyOperates,FS,Barry +LaysTileAt,FS,14,F5,SW +CompanyRevenue,FS,230 +CompanyPaysOutFull,FS,230 +Payout,Barry,138,6,10 +Payout,FS,46,2,10 +PRICE_MOVES_LOG,FS,90,D3,100,E3 + +EndOfOperatingRound,3.2 +ORWorthIncrease,Mark,3.2,311 +ORWorthIncrease,Mike,3.2,670 +ORWorthIncrease,John,3.2,192 +ORWorthIncrease,Barry,3.2,286 +Has,SNCB,64 +Has,NS,924 +Has,KBS,101 +Has,KPEV,100 +Has,KKÖB,8 +Has,FS,66 +Has,SNCF,0 +Has,DR,24 +Has,Mark,586 +Has,Mike,884 +Has,John,251 +Has,Barry,524 +StartStockRound,4 +HasPriority,Mark +BUY_SHARE_LOG,Mark,10,KKÖB,Pool,180 +BUY_SHARE_LOG,Mike,10,DR,Pool,135 +BUY_SHARE_LOG,John,10,SNCB,Pool,110 +BUY_SHARE_LOG,Barry,10,SNCF,Pool,122 +BUY_SHARE_LOG,Mark,10,SNCF,Pool,122 +BUY_SHARE_LOG,Mike,10,NS,Pool,122 +BUY_SHARE_LOG,John,10,SNCB,SNCB,110 +BUY_SHARE_LOG,Barry,10,KPEV,Pool,100 +BUY_SHARE_LOG,Mark,10,NS,Pool,122 +BUY_SHARE_LOG,Mike,10,KPEV,Pool,100 +PASSES,John +PASSES,Barry +BUY_SHARE_LOG,Mark,10,NS,Pool,122 +BUY_SHARE_LOG,Mike,10,KBS,Pool,100 +PASSES,John +PASSES,Barry +PASSES,Mark +PASSES,Mike + +END_SR,4 +SoldOut,KKÖB +PRICE_MOVES_LOG,KKÖB,180,K3,200,K2 +SoldOut,DR +PRICE_MOVES_LOG,DR,135,H3,150,H2 +SoldOut,SNCF +PRICE_MOVES_LOG,SNCF,122,G3,135,G2 +SoldOut,SNCB +PRICE_MOVES_LOG,SNCB,110,G4,122,G3 +Has,SNCB,174 +Has,NS,924 +Has,KBS,101 +Has,KPEV,100 +Has,KKÖB,8 +Has,FS,66 +Has,SNCF,0 +Has,DR,24 +Has,Mark,40 +Has,Mike,427 +Has,John,31 +Has,Barry,302 +START_OR,4.1 + +CompanyOperates,KKÖB,Mike +LaysTileAt,KKÖB,582,P13,SE +LAYS_FREE_TOKEN_ON,KKÖB,P13 +CompanyRevenue,KKÖB,530 +CompanyPaysOutFull,KKÖB,530 +Payout,Mike,265,5,10 +Payout,Barry,106,2,10 +Payout,Mark,106,2,10 +Payout,John,53,1,10 +PRICE_MOVES_LOG,KKÖB,200,K2,220,L2 + +CompanyOperates,DR,Mark +LaysTileAt,DR,611,I6,S +CompanyRevenue,DR,740 +CompanyPaysOutFull,DR,740 +Payout,Mike,74,1,10 +Payout,Barry,148,2,10 +Payout,Mark,444,6,10 +Payout,John,74,1,10 +PRICE_MOVES_LOG,DR,150,H2,165,I2 + +CompanyOperates,SNCF,John +LaysTileAt,SNCF,8,E6,SW +CompanyRevenue,SNCF,500 +CompanyPaysOutFull,SNCF,500 +Payout,Mark,150,3,10 +Payout,Barry,50,1,10 +Payout,John,300,6,10 +PRICE_MOVES_LOG,SNCF,135,G2,150,H2 + +CompanyOperates,NS,Mike +LaysTileAt,NS,611,K10,N +LAYS_FREE_TOKEN_ON,NS,G10 +CompanyRevenue,NS,390 +CompanyPaysOutFull,NS,390 +Payout,Mike,234,6,10 +Payout,Mark,117,3,10 +PRICE_MOVES_LOG,NS,122,H4,135,H3 +BuysTrain,NS,8,IPO,800 + +CompanyOperates,SNCB,Barry +LaysTileAt,SNCB,513,I6,S +LAYS_FREE_TOKEN_ON,SNCB,F5 +CompanyRevenue,SNCB,410 +CompanyPaysOutFull,SNCB,410 +Payout,Mark,82,2,10 +Payout,Barry,246,6,10 +Payout,John,82,2,10 +PRICE_MOVES_LOG,SNCB,122,G3,135,H3 + +CompanyOperates,KPEV,Mike +LaysTileAt,KPEV,7,F11,SW +CompanyRevenue,KPEV,460 +CompanyPaysOutFull,KPEV,460 +Payout,Mike,276,6,10 +Payout,Barry,46,1,10 +PRICE_MOVES_LOG,KPEV,100,E3,110,F3 + +CompanyOperates,KBS,John +CompanyRevenue,KBS,310 +CompanyPaysOutFull,KBS,310 +Payout,Mike,31,1,10 +Payout,John,155,5,10 +PRICE_MOVES_LOG,KBS,100,E3,110,F3 + +CompanyOperates,FS,Barry +LaysTileAt,FS,144,J7,SW +CompanyRevenue,FS,190 +CompanyPaysOutFull,FS,190 +Payout,Barry,114,6,10 +Payout,FS,38,2,10 +PRICE_MOVES_LOG,FS,100,E3,110,F3 + +EndOfOperatingRound,4.1 +ORWorthIncrease,Mark,4.1,1139 +ORWorthIncrease,Mike,4.1,1143 +ORWorthIncrease,John,4.1,865 +ORWorthIncrease,Barry,4.1,943 +Has,SNCB,174 +Has,NS,124 +Has,KBS,101 +Has,KPEV,100 +Has,KKÖB,8 +Has,FS,104 +Has,SNCF,0 +Has,DR,24 +Has,Mark,939 +Has,Mike,1307 +Has,John,695 +Has,Barry,1012 +START_OR,4.2 + +CompanyOperates,KKÖB,Mike +LaysTileAt,KKÖB,513,K10,S +CompanyRevenue,KKÖB,560 +CompanyPaysOutFull,KKÖB,560 +Payout,Mike,280,5,10 +Payout,Barry,112,2,10 +Payout,Mark,112,2,10 +Payout,John,56,1,10 +PRICE_MOVES_LOG,KKÖB,220,L2,245,M2 + +CompanyOperates,DR,Mark +LaysTileAt,DR,578,L7,NW +CompanyRevenue,DR,780 +CompanyPaysOutFull,DR,780 +Payout,Mike,78,1,10 +Payout,Barry,156,2,10 +Payout,Mark,468,6,10 +Payout,John,78,1,10 +PRICE_MOVES_LOG,DR,165,I2,180,J2 + +CompanyOperates,SNCF,John +LaysTileAt,SNCF,611,G4,NW +CompanyRevenue,SNCF,420 +CompanyPaysOutFull,SNCF,420 +Payout,Mark,126,3,10 +Payout,Barry,42,1,10 +Payout,John,252,6,10 +PRICE_MOVES_LOG,SNCF,150,H2,165,I2 + +CompanyOperates,NS,Mike +LaysTileAt,NS,15,R9,SW +LAYS_FREE_TOKEN_ON,NS,R9 +CompanyRevenue,NS,870 +CompanyPaysOutFull,NS,870 +Payout,Mike,522,6,10 +Payout,Mark,261,3,10 +PRICE_MOVES_LOG,NS,135,H3,150,I3 + +CompanyOperates,SNCB,Barry +LaysTileAt,SNCB,513,G4,S +CompanyRevenue,SNCB,520 +CompanyPaysOutFull,SNCB,520 +Payout,Mark,104,2,10 +Payout,Barry,312,6,10 +Payout,John,104,2,10 +PRICE_MOVES_LOG,SNCB,135,H3,150,I3 + +CompanyOperates,KPEV,Mike +LaysTileAt,KPEV,143,D3,SW +CompanyRevenue,KPEV,460 +CompanyPaysOutFull,KPEV,460 +Payout,Mike,276,6,10 +Payout,Barry,46,1,10 +PRICE_MOVES_LOG,KPEV,110,F3,122,G3 + +CompanyOperates,KBS,John +LaysTileAt,KBS,9,H5,NW +CompanyRevenue,KBS,420 +CompanyPaysOutFull,KBS,420 +Payout,Mike,42,1,10 +Payout,John,210,5,10 +PRICE_MOVES_LOG,KBS,110,F3,122,G3 + +CompanyOperates,FS,Barry +LaysTileAt,FS,142,C6,SE +CompanyRevenue,FS,440 +CompanyPaysOutFull,FS,440 +Payout,Barry,264,6,10 +Payout,FS,88,2,10 +PRICE_MOVES_LOG,FS,110,F3,122,G3 + +EndOfOperatingRound,4.2 +ORWorthIncrease,Mark,4.2,1331 +ORWorthIncrease,Mike,4.2,1512 +ORWorthIncrease,John,4.2,920 +ORWorthIncrease,Barry,4.2,1201 +Has,SNCB,174 +Has,NS,124 +Has,KBS,101 +Has,KPEV,100 +Has,KKÖB,8 +Has,FS,192 +Has,SNCF,0 +Has,DR,24 +Has,Mark,2010 +Has,Mike,2505 +Has,John,1395 +Has,Barry,1944 +StartStockRound,5 +HasPriority,John +BUY_SHARE_LOG,John,10,NS,Pool,150 +SELL_SHARE_LOG,Barry,10,SNCF,165 +PRICE_MOVES_LOG,SNCF,165,I2,150,I3 +BUY_SHARE_LOG,Barry,10,KPEV,Pool,122 +BUY_SHARE_LOG,Mark,10,SNCF,Pool,150 +SELL_SHARE_LOG,Mike,10,KBS,122 +PRICE_MOVES_LOG,KBS,122,G3,110,G4 +BUY_SHARE_LOG,Mike,10,FS,Pool,122 +BUY_SHARE_LOG,John,10,FS,Pool,122 +PASSES,Barry +PASSES,Mark +PASSES,Mike +BUY_SHARE_LOG,John,10,FS,FS,122 +PASSES,Barry +PASSES,Mark +PASSES,Mike +PASSES,John + +END_SR,5 +SoldOut,KKÖB +PRICE_MOVES_LOG,KKÖB,245,M2,270,M1 +SoldOut,DR +PRICE_MOVES_LOG,DR,180,J2,200,J1 +SoldOut,NS +PRICE_MOVES_LOG,NS,150,I3,165,I2 +SoldOut,SNCB +PRICE_MOVES_LOG,SNCB,150,I3,165,I2 +SoldOut,SNCF +PRICE_MOVES_LOG,SNCF,150,I3,165,I2 +Has,SNCB,174 +Has,NS,124 +Has,KBS,101 +Has,KPEV,100 +Has,KKÖB,8 +Has,FS,314 +Has,SNCF,0 +Has,DR,24 +Has,Mark,1860 +Has,Mike,2505 +Has,John,1001 +Has,Barry,1987 +START_OR,5.1 + +CompanyOperates,KKÖB,Mike +LaysTileAt,KKÖB,611,R9,SW +CompanyRevenue,KKÖB,580 +CompanyPaysOutFull,KKÖB,580 +Payout,Mike,290,5,10 +Payout,Mark,116,2,10 +Payout,Barry,116,2,10 +Payout,John,58,1,10 +PRICE_MOVES_LOG,KKÖB,270,M1,300,N1 + +CompanyOperates,DR,Mark +LaysTileAt,DR,582,L7,S +CompanyRevenue,DR,930 +CompanyPaysOutFull,DR,930 +Payout,Mike,93,1,10 +Payout,Mark,558,6,10 +Payout,Barry,186,2,10 +Payout,John,93,1,10 +PRICE_MOVES_LOG,DR,200,J1,220,K1 + +CompanyOperates,NS,Mike +LaysTileAt,NS,513,R9,S +LAYS_FREE_TOKEN_ON,NS,S8 +CompanyRevenue,NS,970 +CompanyPaysOutFull,NS,970 +Payout,Mike,582,6,10 +Payout,Mark,291,3,10 +Payout,John,97,1,10 +PRICE_MOVES_LOG,NS,165,I2,180,J2 + +CompanyOperates,SNCB,Barry +LaysTileAt,SNCB,58,E4,S +CompanyRevenue,SNCB,600 +CompanyPaysOutFull,SNCB,600 +Payout,Mark,120,2,10 +Payout,Barry,360,6,10 +Payout,John,120,2,10 +PRICE_MOVES_LOG,SNCB,165,I2,180,J2 + +CompanyOperates,SNCF,John +LaysTileAt,SNCF,143,G8,SE +LAYS_FREE_TOKEN_ON,SNCF,L7 +CompanyRevenue,SNCF,570 +CompanyPaysOutFull,SNCF,570 +Payout,Mark,228,4,10 +Payout,John,342,6,10 +PRICE_MOVES_LOG,SNCF,165,I2,180,J2 + +CompanyOperates,KPEV,Mike +LaysTileAt,KPEV,142,E4,SW +CompanyRevenue,KPEV,470 +CompanyPaysOutFull,KPEV,470 +BankIsBrokenReportText +Payout,Mike,282,6,10 +Payout,Barry,94,2,10 +PRICE_MOVES_LOG,KPEV,122,G3,135,H3 + +CompanyOperates,FS,Barry +LaysTileAt,FS,80,C10,SW +CompanyRevenue,FS,460 +CompanyPaysOutFull,FS,460 +Payout,Mike,46,1,10 +Payout,Barry,276,6,10 +Payout,John,92,2,10 +Payout,FS,46,1,10 +PRICE_MOVES_LOG,FS,122,G3,135,H3 + +CompanyOperates,KBS,John +LaysTileAt,KBS,8,E8,S +CompanyRevenue,KBS,440 +CompanyPaysOutFull,KBS,440 +Payout,John,220,5,10 +PRICE_MOVES_LOG,KBS,110,G4,122,H4 + +EndOfOperatingRound,5.1 +ORWorthIncrease,Mark,5.1,1628 +ORWorthIncrease,Mike,5.1,1644 +ORWorthIncrease,John,5.1,1293 +ORWorthIncrease,Barry,5.1,1326 +Has,SNCB,174 +Has,NS,124 +Has,KBS,101 +Has,KPEV,100 +Has,KKÖB,8 +Has,FS,360 +Has,SNCF,0 +Has,DR,24 +Has,Mark,3173 +Has,Mike,3798 +Has,John,2023 +Has,Barry,3019 +START_OR,5.2 + +CompanyOperates,KKÖB,Mike +CompanyRevenue,KKÖB,600 +CompanyPaysOutFull,KKÖB,600 +Payout,Mike,300,5,10 +Payout,Mark,120,2,10 +Payout,Barry,120,2,10 +Payout,John,60,1,10 +PRICE_MOVES_LOG,KKÖB,300,N1,330,O1 + +CompanyOperates,DR,Mark +LaysTileAt,DR,147,E4,SW +CompanyRevenue,DR,960 +CompanyPaysOutFull,DR,960 +Payout,Mike,96,1,10 +Payout,Mark,576,6,10 +Payout,Barry,192,2,10 +Payout,John,96,1,10 +PRICE_MOVES_LOG,DR,220,K1,245,L1 + +CompanyOperates,NS,Mike +LaysTileAt,NS,147,G8,SW +CompanyRevenue,NS,980 +CompanyPaysOutFull,NS,980 +Payout,Mike,588,6,10 +Payout,Mark,294,3,10 +Payout,John,98,1,10 +PRICE_MOVES_LOG,NS,180,J2,200,K2 + +CompanyOperates,SNCB,Barry +LaysTileAt,SNCB,142,D11,S +CompanyRevenue,SNCB,550 +CompanyPaysOutFull,SNCB,550 +Payout,Mark,110,2,10 +Payout,Barry,330,6,10 +Payout,John,110,2,10 +PRICE_MOVES_LOG,SNCB,180,J2,200,K2 + +CompanyOperates,SNCF,John +LaysTileAt,SNCF,8,D7,N +CompanyRevenue,SNCF,670 +CompanyPaysOutFull,SNCF,670 +Payout,Mark,268,4,10 +Payout,John,402,6,10 +PRICE_MOVES_LOG,SNCF,180,J2,200,K2 + +CompanyOperates,KPEV,Mike +LaysTileAt,KPEV,146,C6,NW +CompanyRevenue,KPEV,500 +CompanyPaysOutFull,KPEV,500 +Payout,Mike,300,6,10 +Payout,Barry,100,2,10 +PRICE_MOVES_LOG,KPEV,135,H3,150,I3 + +CompanyOperates,FS,Barry +LaysTileAt,FS,611,F5,NW +LAYS_FREE_TOKEN_ON,FS,G4 +CompanyRevenue,FS,540 +CompanyPaysOutFull,FS,540 +Payout,Mike,54,1,10 +Payout,Barry,324,6,10 +Payout,John,108,2,10 +Payout,FS,54,1,10 +PRICE_MOVES_LOG,FS,135,H3,150,I3 + +CompanyOperates,KBS,John +LaysTileAt,KBS,146,D11,N +CompanyRevenue,KBS,470 +CompanyPaysOutFull,KBS,470 +Payout,John,235,5,10 +PRICE_MOVES_LOG,KBS,122,H4,135,H3 + +EndOfOperatingRound,5.2 +ORWorthIncrease,Mark,5.2,1758 +ORWorthIncrease,Mike,5.2,1738 +ORWorthIncrease,John,5.2,1439 +ORWorthIncrease,Barry,5.2,1416 +Has,SNCB,174 +Has,NS,124 +Has,KBS,101 +Has,KPEV,100 +Has,KKÖB,8 +Has,FS,414 +Has,SNCF,0 +Has,DR,24 +Has,Mark,4541 +Has,Mike,5136 +Has,John,3132 +Has,Barry,4085 +GameOver +EoGWinnerMike! +EoGFinalRanking : +1. 9281 Mike +2. 8471 Mark +3. 7635 Barry +4. 6482 John diff --git a/src/test/resources/data/real/18VAZ22.rails b/src/test/resources/data/real/18VAZ22.rails new file mode 100644 index 0000000000000000000000000000000000000000..bc45f8dcbee001d3235eef379aec9ccf6960f09d GIT binary patch literal 28610 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