From 5b7728db581455a7783e479cc27f2ac66d1843c1 Mon Sep 17 00:00:00 2001 From: EspenTinius Date: Mon, 25 May 2026 23:20:05 +0200 Subject: [PATCH 1/2] slik --- .../ntnu/idi/idatt2003/g40/mappe/Main.java | 31 ++ .../idatt2003/g40/mappe/engine/Exchange.java | 49 +++ .../g40/mappe/engine/TransactionArchive.java | 12 + .../g40/mappe/model/OwnedShareData.java | 81 ++++ .../idi/idatt2003/g40/mappe/model/Player.java | 34 ++ .../idatt2003/g40/mappe/model/Portfolio.java | 12 + .../idatt2003/g40/mappe/model/SaveGame.java | 103 ++++- .../idi/idatt2003/g40/mappe/model/Stock.java | 23 + .../g40/mappe/model/TransactionData.java | 115 +++++ .../g40/mappe/service/GameStateLoader.java | 302 +++++++++++++ .../g40/mappe/service/SaveGameService.java | 411 +++++++++++++++++- .../g40/mappe/service/event/EventType.java | 42 +- .../view/playgame/PlayGameController.java | 50 ++- .../dashboard/DashBoardController.java | 26 +- .../financialsummary/SummaryController.java | 53 ++- .../view/widgets/market/MarketController.java | 22 +- .../view/widgets/stats/StatsController.java | 41 +- .../view/widgets/topbar/TopBarController.java | 30 +- .../transactions/TransactionsController.java | 58 ++- src/main/resources/saves/Newbie.json | 29 +- 20 files changed, 1488 insertions(+), 36 deletions(-) create mode 100644 src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/OwnedShareData.java create mode 100644 src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/TransactionData.java create mode 100644 src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/GameStateLoader.java diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/Main.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/Main.java index 3caea4b..cec044e 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/Main.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/Main.java @@ -2,9 +2,11 @@ import edu.ntnu.idi.idatt2003.g40.mappe.engine.Exchange; import edu.ntnu.idi.idatt2003.g40.mappe.model.Player; +import edu.ntnu.idi.idatt2003.g40.mappe.model.SaveGame; import edu.ntnu.idi.idatt2003.g40.mappe.model.Stock; import edu.ntnu.idi.idatt2003.g40.mappe.service.FileConverter; import edu.ntnu.idi.idatt2003.g40.mappe.service.FileParser; +import edu.ntnu.idi.idatt2003.g40.mappe.service.GameStateLoader; import edu.ntnu.idi.idatt2003.g40.mappe.service.SaveGameService; import edu.ntnu.idi.idatt2003.g40.mappe.service.event.EventManager; import edu.ntnu.idi.idatt2003.g40.mappe.utils.ConfigValues; @@ -94,6 +96,11 @@ public void start(final Stage stage) throws Exception { Exchange exchange = new Exchange("Exchange", stocksInFile); Player player = new Player("Player 1", new BigDecimal("10000")); + // Bridges the save list to the live player + exchange. Subscribes + // to LOAD_SAVE; mutates state; publishes STATE_RESET for widgets. + GameStateLoader gameStateLoader = new GameStateLoader( + player, exchange, stocksInFile, eventManager); + // Main menu MainMenuView mainMenuView = new MainMenuView(); new MainMenuController(mainMenuView, eventManager); @@ -210,6 +217,30 @@ public void start(final Stage stage) throws Exception { new InGameSettingsController(inGameSettingsView, eventManager, inGameView); topBarController.setSettingsAction(inGameSettingsController::show); + // Auto-save the currently active save when the player quits back + // to the main menu. Silent if no save is active (i.e. the user + // got into the in-game scene without going through the save list). + topBarController.setOnQuitToMainMenu(() -> { + System.out.println("[auto-save] Quit triggered, attempting snapshot..."); + SaveGame snapshot = gameStateLoader.snapshotActiveSave(); + if (snapshot == null) { + System.out.println("[auto-save] No active save - nothing to write."); + return; + } + System.out.println("[auto-save] Snapshot built for '" + snapshot.getName() + + "', balance=" + snapshot.getBalance() + + ", week=" + snapshot.getWeek() + + ", shares=" + snapshot.getOwnedShares().size() + + ", txns=" + snapshot.getTransactions().size()); + try { + saveGameService.saveGame(snapshot); + System.out.println("[auto-save] Wrote save '" + snapshot.getName() + "' to disk."); + } catch (Exception e) { + System.err.println("[auto-save] Failed: " + e.getMessage()); + e.printStackTrace(); + } + }); + // Register all views viewManager.addView(mainMenuView); viewManager.addView(playGameView); diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/Exchange.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/Exchange.java index b73f634..dc0d271 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/Exchange.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/Exchange.java @@ -103,6 +103,55 @@ public IntegerProperty weekProperty() { return week; } + /** + * Sets the current week directly. + * + *

+ * Used by the save-loading flow when applying a + * {@link edu.ntnu.idi.idatt2003.g40.mappe.model.SaveGame} to the + * exchange. The {@link IntegerProperty} fires a change event so any + * listeners that re-render on week changes refresh themselves. + *

+ * + * @param newWeek the value to set the week to. + * + * @throws IllegalArgumentException if newWeek is below 1. + * */ + public void setWeek(final int newWeek) throws IllegalArgumentException { + if (newWeek < 1) { + throw new IllegalArgumentException("Week must be at least 1!"); + } + week.set(newWeek); + } + + /** + * Resets every known stock on the exchange back to a baseline sales + * price. + * + *

+ * Wipes the price history of each stock that has a matching entry + * in {@code baselinePrices} and re-seeds it with the given value. + * Used by the save-loading flow so the exchange the loaded save + * sees matches the baseline the save started from, regardless of + * how the simulation had drifted before. + *

+ * + * @param baselinePrices map of symbol to the price to seed with. + * Symbols that aren't on this exchange are + * skipped. + * */ + public void resetStocksTo(final Map baselinePrices) { + if (baselinePrices == null) { + return; + } + for (Map.Entry entry : baselinePrices.entrySet()) { + Stock stock = stockMap.get(entry.getKey()); + if (stock != null) { + stock.resetPrices(entry.getValue()); + } + } + } + /** * Method for checking whether exchange has a stock. * diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/TransactionArchive.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/TransactionArchive.java index dadc496..2716ab4 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/TransactionArchive.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/TransactionArchive.java @@ -33,6 +33,18 @@ public boolean add(final Transaction transaction) { return transactions.add(transaction); } + /** + * Removes every transaction from the archive. + * + *

Used by the save-loading flow when applying a + * {@link edu.ntnu.idi.idatt2003.g40.mappe.model.SaveGame} to an + * existing player, to make sure no transactions from the previous + * save remain.

+ * */ + public void clear() { + transactions.clear(); + } + /** * Checks whether the archive is empty. * diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/OwnedShareData.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/OwnedShareData.java new file mode 100644 index 0000000..92aaa30 --- /dev/null +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/OwnedShareData.java @@ -0,0 +1,81 @@ +package edu.ntnu.idi.idatt2003.g40.mappe.model; + +import java.math.BigDecimal; + +/** + * Immutable data record describing a single owned share in a {@link SaveGame}. + * + *

+ * Holds just enough information to recreate a {@link Share} when the save is + * loaded: the stock symbol, the quantity owned and the original purchase + * price. The actual {@link Stock} object is looked up on the live + * {@link edu.ntnu.idi.idatt2003.g40.mappe.engine.Exchange} when the save is + * applied. + *

+ * + *

+ * Kept as a separate data type (rather than serialising {@link Share} + * directly) so the on-disk format stays decoupled from the runtime model + * and is easy to read/write through the hand-written JSON parser in + * {@link edu.ntnu.idi.idatt2003.g40.mappe.service.SaveGameService}. + *

+ */ +public final class OwnedShareData { + + /** Stock symbol this share is for. */ + private final String symbol; + + /** Quantity of the stock owned. */ + private final BigDecimal quantity; + + /** Price per unit at the time of purchase. */ + private final BigDecimal purchasePrice; + + /** + * Constructor. + * + * @param symbol the stock symbol. + * @param quantity the quantity owned. + * @param purchasePrice the price per unit at purchase time. + * + * @throws IllegalArgumentException if any argument is null. + * */ + public OwnedShareData(final String symbol, + final BigDecimal quantity, + final BigDecimal purchasePrice) + throws IllegalArgumentException { + if (symbol == null || quantity == null || purchasePrice == null) { + throw new IllegalArgumentException("Invalid owned share data!"); + } + this.symbol = symbol; + this.quantity = quantity; + this.purchasePrice = purchasePrice; + } + + /** + * Getter method for the symbol. + * + * @return the stock symbol. + * */ + public String getSymbol() { + return symbol; + } + + /** + * Getter method for the quantity. + * + * @return the quantity owned. + * */ + public BigDecimal getQuantity() { + return quantity; + } + + /** + * Getter method for the purchase price. + * + * @return the price per unit at purchase time. + * */ + public BigDecimal getPurchasePrice() { + return purchasePrice; + } +} diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Player.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Player.java index 9cac71e..7f79db4 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Player.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Player.java @@ -123,6 +123,40 @@ public void withdrawMoney(final BigDecimal amount) { money = money.subtract(amount); } + /** + * Replaces the players current balance with the given value. + * + *

+ * Used by the save-loading flow when applying a {@link SaveGame} to + * an existing player. Unlike {@link #addMoney} / {@link #withdrawMoney}, + * this fully overwrites the current value rather than adjusting it. + *

+ * + * @param newMoney the value to set the balance to. + * + * @throws IllegalArgumentException if newMoney is null. + * */ + public void setMoney(final BigDecimal newMoney) throws IllegalArgumentException { + if (newMoney == null) { + throw new IllegalArgumentException("Money cannot be null!"); + } + money = newMoney; + } + + /** + * Re-publishes the players current money and net-worth to the + * {@link FloatProperty} listeners. + * + *

+ * Used after the save-loading flow has mutated the player state + * (money, portfolio, archive) so listeners across the UI refresh. + *

+ * */ + public void refreshProperties() { + networthAsFloatProp.setValue(getNetWorth().floatValue()); + moneyAsFloatProp.setValue(money); + } + /** * Returns the players portfolio. * diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Portfolio.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Portfolio.java index 483adb8..0f232bd 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Portfolio.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Portfolio.java @@ -72,6 +72,18 @@ public List getShares() { return List.copyOf(shares); } + /** + * Removes every share from the portfolio. + * + *

Used by the save-loading flow when applying a + * {@link edu.ntnu.idi.idatt2003.g40.mappe.model.SaveGame} to an + * existing player, to make sure no shares from the previous save + * remain.

+ * */ + public void clear() { + shares.clear(); + } + /** * Returns an immutable snapshot of all shares whose * stock symbol matches the given symbol. diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/SaveGame.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/SaveGame.java index ff00e77..9dc97ec 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/SaveGame.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/SaveGame.java @@ -1,18 +1,29 @@ package edu.ntnu.idi.idatt2003.g40.mappe.model; +import java.util.Collections; +import java.util.List; + /** * Represents one save game entry. * *

- * Holds the display name, current balance, starting capital and - * (optionally) the path to a custom stock data file for a single - * saved game. + * Holds the display name, current balance, starting capital, + * (optionally) the path to a custom stock data file, the in-game week, + * the list of owned shares and the list of committed transactions for a + * single saved game. *

* *

* When {@link #getStockDataPath()} returns {@code null} the save is * expected to be loaded with the default bundled stock data file. *

+ * + *

+ * Older saves on disk may not contain {@link #getWeek()}, + * {@link #getOwnedShares()} or {@link #getTransactions()}; in that case + * the convenience constructor defaults week to 1 and the two lists to + * empty, so the save still loads cleanly. + *

*/ public class SaveGame { @@ -31,6 +42,15 @@ public class SaveGame { * */ private final String stockDataPath; + /** In-game week when the save was last written. */ + private final int week; + + /** Shares the player owned when the save was last written. */ + private final List ownedShares; + + /** Committed transactions when the save was last written. */ + private final List transactions; + /** * Full constructor. * @@ -39,31 +59,73 @@ public class SaveGame { * @param startingCapital the starting capital chosen on creation. * @param stockDataPath absolute path to a custom stock data file, * or {@code null} to use the default file. + * @param week the in-game week when the save was written. + * @param ownedShares the shares the player owned (may be null; + * treated as empty). + * @param transactions the committed transactions (may be null; + * treated as empty). */ public SaveGame(final String name, final double balance, final double startingCapital, - final String stockDataPath) { + final String stockDataPath, + final int week, + final List ownedShares, + final List transactions) { this.name = name; this.balance = balance; this.startingCapital = startingCapital; this.stockDataPath = stockDataPath; + this.week = week; + this.ownedShares = (ownedShares != null) + ? List.copyOf(ownedShares) + : Collections.emptyList(); + this.transactions = (transactions != null) + ? List.copyOf(transactions) + : Collections.emptyList(); + } + + /** + * Convenience constructor matching the original "name + balance + capital + * + stockDataPath" format used by the create-game flow. + * + *

+ * Week defaults to 1, owned shares and transactions default to empty + * lists. This keeps the create-game flow unchanged while older save + * files (which don't yet contain gameplay data) load with sensible + * defaults. + *

+ * + * @param name the display name of the save. + * @param balance the current balance value. + * @param startingCapital the starting capital chosen on creation. + * @param stockDataPath absolute path to a custom stock data file, + * or {@code null} to use the default file. + * */ + public SaveGame(final String name, + final double balance, + final double startingCapital, + final String stockDataPath) { + this(name, balance, startingCapital, stockDataPath, + 1, Collections.emptyList(), Collections.emptyList()); } /** * Convenience constructor matching the legacy "name + balance" format. * *

- * Starting capital is defaulted to the balance value and + * Starting capital is defaulted to the balance value, * {@code stockDataPath} is left {@code null} so the default stock - * data file is used. + * data file is used, week defaults to 1 and the two gameplay lists + * default to empty. *

* * @param name the display name of the save. * @param balance the current balance value. */ public SaveGame(final String name, final double balance) { - this(name, balance, balance, null); + this(name, balance, balance, null, + 1, Collections.emptyList(), Collections.emptyList()); } /** @@ -102,4 +164,31 @@ public double getStartingCapital() { public String getStockDataPath() { return stockDataPath; } + + /** + * Getter method for the in-game week. + * + * @return the week the save was last written at. + * */ + public int getWeek() { + return week; + } + + /** + * Getter method for the owned shares. + * + * @return an immutable list of {@link OwnedShareData} entries. + * */ + public List getOwnedShares() { + return ownedShares; + } + + /** + * Getter method for the committed transactions. + * + * @return an immutable list of {@link TransactionData} entries. + * */ + public List getTransactions() { + return transactions; + } } diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Stock.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Stock.java index 1ca9664..abd7273 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Stock.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Stock.java @@ -116,6 +116,29 @@ public void addNewSalesPrice(final BigDecimal price) } } + /** + * Wipes the price history and re-seeds it with a single baseline value. + * + *

+ * Used by the save-loading flow so each save can start the simulation + * from a known baseline rather than inheriting the drift accumulated + * by the previous session. + *

+ * + * @param baseline the price to seed the history with. + * + * @throws IllegalArgumentException if baseline is null or zero. + * */ + public void resetPrices(final BigDecimal baseline) throws IllegalArgumentException { + if (baseline == null || baseline.intValue() == 0) { + throw new IllegalArgumentException( + "Invalid baseline price for stock: " + getSymbol()); + } + prices.clear(); + prices.add(baseline); + fortune = 0; + } + /** * Returns list of all prices for this stock. * diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/TransactionData.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/TransactionData.java new file mode 100644 index 0000000..45f1bde --- /dev/null +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/TransactionData.java @@ -0,0 +1,115 @@ +package edu.ntnu.idi.idatt2003.g40.mappe.model; + +import edu.ntnu.idi.idatt2003.g40.mappe.service.TransactionType; +import java.math.BigDecimal; + +/** + * Immutable data record describing a single committed transaction in a + * {@link SaveGame}. + * + *

+ * Holds the {@link TransactionType} (purchase or sale), the symbol and + * quantity traded, the price per unit at the time of the transaction, and + * the in-game week it took place. Used by the + * {@link edu.ntnu.idi.idatt2003.g40.mappe.service.SaveGameService} to + * persist the {@link edu.ntnu.idi.idatt2003.g40.mappe.engine.TransactionArchive} + * across sessions. + *

+ * + *

+ * Kept as a separate data type (rather than serialising + * {@link Transaction} subclasses directly) so the on-disk format stays + * decoupled from the runtime model and is easy to read/write through the + * hand-written JSON parser. + *

+ */ +public final class TransactionData { + + /** Type of transaction (purchase or sale). */ + private final TransactionType type; + + /** Stock symbol this transaction is for. */ + private final String symbol; + + /** Quantity of the stock involved in the transaction. */ + private final BigDecimal quantity; + + /** Price per unit at the time of the transaction. */ + private final BigDecimal price; + + /** In-game week this transaction took place during. */ + private final int week; + + /** + * Constructor. + * + * @param type the transaction type. + * @param symbol the stock symbol. + * @param quantity the quantity traded. + * @param price the price per unit at the time of the transaction. + * @param week the in-game week the transaction took place. + * + * @throws IllegalArgumentException if any reference argument is null. + * */ + public TransactionData(final TransactionType type, + final String symbol, + final BigDecimal quantity, + final BigDecimal price, + final int week) + throws IllegalArgumentException { + if (type == null || symbol == null + || quantity == null || price == null) { + throw new IllegalArgumentException("Invalid transaction data!"); + } + this.type = type; + this.symbol = symbol; + this.quantity = quantity; + this.price = price; + this.week = week; + } + + /** + * Getter method for the transaction type. + * + * @return the {@link TransactionType}. + * */ + public TransactionType getType() { + return type; + } + + /** + * Getter method for the symbol. + * + * @return the stock symbol. + * */ + public String getSymbol() { + return symbol; + } + + /** + * Getter method for the quantity. + * + * @return the quantity traded. + * */ + public BigDecimal getQuantity() { + return quantity; + } + + /** + * Getter method for the price. + * + * @return the price per unit at the time of the transaction. + * */ + public BigDecimal getPrice() { + return price; + } + + /** + * Getter method for the week. + * + * @return the in-game week the transaction took place. + * */ + public int getWeek() { + return week; + } +} diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/GameStateLoader.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/GameStateLoader.java new file mode 100644 index 0000000..8cbd29e --- /dev/null +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/GameStateLoader.java @@ -0,0 +1,302 @@ +package edu.ntnu.idi.idatt2003.g40.mappe.service; + +import edu.ntnu.idi.idatt2003.g40.mappe.engine.Exchange; +import edu.ntnu.idi.idatt2003.g40.mappe.engine.TransactionArchive; +import edu.ntnu.idi.idatt2003.g40.mappe.model.OwnedShareData; +import edu.ntnu.idi.idatt2003.g40.mappe.model.Player; +import edu.ntnu.idi.idatt2003.g40.mappe.model.Portfolio; +import edu.ntnu.idi.idatt2003.g40.mappe.model.Purchase; +import edu.ntnu.idi.idatt2003.g40.mappe.model.Sale; +import edu.ntnu.idi.idatt2003.g40.mappe.model.SaveGame; +import edu.ntnu.idi.idatt2003.g40.mappe.model.Share; +import edu.ntnu.idi.idatt2003.g40.mappe.model.Stock; +import edu.ntnu.idi.idatt2003.g40.mappe.model.Transaction; +import edu.ntnu.idi.idatt2003.g40.mappe.model.TransactionData; +import edu.ntnu.idi.idatt2003.g40.mappe.service.event.EventData; +import edu.ntnu.idi.idatt2003.g40.mappe.service.event.EventManager; +import edu.ntnu.idi.idatt2003.g40.mappe.service.event.EventPublisher; +import edu.ntnu.idi.idatt2003.g40.mappe.service.event.EventSubscriber; +import edu.ntnu.idi.idatt2003.g40.mappe.service.event.EventType; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Service that applies a {@link SaveGame} to the active {@link Player} + * and {@link Exchange}. + * + *

+ * Listens for {@link EventType#LOAD_SAVE} events. When one fires the + * service: + *

+ *
    + *
  • Resets the stock prices on the exchange to the baseline they + * had when the application started.
  • + *
  • Resets the players money, portfolio and transaction archive.
  • + *
  • Re-applies the saved owned shares and transactions on top of + * that clean state.
  • + *
  • Sets the in-game week to the value stored in the save.
  • + *
  • Publishes a {@link EventType#STATE_RESET} event so widget + * controllers can refresh whatever derived state they hold + * (chart histories, week selectors, etc.).
  • + *
+ * + *

+ * Implements both {@link EventSubscriber} (to receive + * {@code LOAD_SAVE}) and {@link EventPublisher} (to emit + * {@code STATE_RESET}). Tracks the {@link SaveGame} most recently + * loaded so callers (the auto-save flow on quit) can ask which save + * is currently active. + *

+ */ +public final class GameStateLoader + implements EventSubscriber, EventPublisher { + + /** Active {@link Player} instance being mutated on load. */ + private final Player player; + + /** Active {@link Exchange} instance being mutated on load. */ + private final Exchange exchange; + + /** Used to publish {@link EventType#STATE_RESET} after a load. */ + private final EventManager eventManager; + + /** + * Snapshot of every stock's price at construction time. + * + *

+ * Captured so each save load can rewind the exchange to a known + * baseline regardless of how the simulation has drifted in this + * session. + *

+ * */ + private final Map baselinePrices; + + /** + * The {@link SaveGame} most recently loaded, or {@code null} if no + * save has been loaded yet. + * + *

Used by the auto-save flow so it can write back to the same + * file the player opened.

+ * */ + private SaveGame activeSave; + + /** + * Constructor. + * + * @param player the {@link Player} to apply saves to. + * @param exchange the {@link Exchange} to apply saves to. + * @param stockList the list of stocks from which to capture a + * baseline price snapshot. + * @param eventManager the active {@link EventManager}; the loader + * subscribes to {@link EventType#LOAD_SAVE} and + * publishes {@link EventType#STATE_RESET} + * through it. + * + * @throws IllegalArgumentException if any argument is null. + * */ + public GameStateLoader(final Player player, + final Exchange exchange, + final List stockList, + final EventManager eventManager) + throws IllegalArgumentException { + if (player == null || exchange == null + || stockList == null || eventManager == null) { + throw new IllegalArgumentException("Invalid GameStateLoader arguments!"); + } + this.player = player; + this.exchange = exchange; + this.eventManager = eventManager; + this.baselinePrices = captureBaseline(stockList); + eventManager.addSubscriber(this, EventType.LOAD_SAVE); + } + + /** + * Getter method for the {@link SaveGame} most recently loaded. + * + * @return the active save, or {@code null} if none has been loaded. + * */ + public SaveGame getActiveSave() { + return activeSave; + } + + /** + * Builds a fresh {@link SaveGame} snapshot of the current player + * and exchange state, using the active save's name and starting + * capital as identity. + * + *

+ * Used by the auto-save flow when the player quits the in-game view + * back to the main menu. Returns {@code null} if no save is active + * (i.e. the player hasn't opened a save in this session). + *

+ * + * @return a snapshot {@link SaveGame}, or {@code null}. + * */ + public SaveGame snapshotActiveSave() { + if (activeSave == null) { + return null; + } + List shares = new ArrayList<>(); + for (Share s : player.getPortfolio().getShares()) { + shares.add(new OwnedShareData( + s.getStock().getSymbol(), + s.getQuantity(), + s.getPurchasePrice())); + } + + List txns = new ArrayList<>(); + for (Transaction t : player.getTransactionArchive().getTransactions()) { + TransactionType type = (t instanceof Sale) + ? TransactionType.SALE + : TransactionType.PURCHASE; + txns.add(new TransactionData( + type, + t.getShare().getStock().getSymbol(), + t.getShare().getQuantity(), + t.getShare().getPurchasePrice(), + t.getWeek())); + } + + return new SaveGame( + activeSave.getName(), + player.getMoney().doubleValue(), + activeSave.getStartingCapital(), + activeSave.getStockDataPath(), + exchange.getWeek(), + shares, + txns); + } + + /** {@inheritDoc} */ + @Override + public void handleEvent(final EventData data) { + if (data == null || !(data.data() instanceof SaveGame save)) { + throw new IllegalArgumentException( + "LOAD_SAVE event payload must be a SaveGame!"); + } + // Tell widget controllers to wipe their per-week history before + // we start the replay - otherwise history from the previous + // session leaks into the new chart. + invoke(new EventData<>(EventType.PRE_LOAD_SAVE, save), eventManager); + applySave(save); + invoke(new EventData<>(EventType.STATE_RESET, save), eventManager); + } + + /** {@inheritDoc} */ + @Override + public void invoke(final EventData data, + final EventManager manager) { + manager.invokeEvent(data); + } + + /** + * Applies the contents of a {@link SaveGame} to the live player and + * exchange. + * + *

+ * Resets stock prices to the captured baseline, then wipes and + * re-seeds the player's money, portfolio and transaction archive + * from the save, and finally sets the in-game week. + *

+ * + * @param save the {@link SaveGame} to apply. + * */ + private void applySave(final SaveGame save) { + System.out.println("[loader] applySave: name=" + save.getName() + + ", balance=" + save.getBalance() + + ", week=" + save.getWeek() + + ", shares=" + save.getOwnedShares().size() + + ", txns=" + save.getTransactions().size()); + + // 1. Roll the exchange's prices back to the captured baseline and + // the exchange's week back to 1, so the simulation starts + // fresh. + exchange.resetStocksTo(baselinePrices); + exchange.setWeek(1); + + // 2. Reset the player to an empty state with the saved starting + // capital. We use starting capital here (rather than the + // final saved balance) so the per-week net-worth samples + // collected by widget listeners during the advances below + // have a meaningful baseline. + Portfolio portfolio = player.getPortfolio(); + portfolio.clear(); + TransactionArchive archive = player.getTransactionArchive(); + archive.clear(); + player.setMoney(BigDecimal.valueOf(save.getStartingCapital())); + player.refreshProperties(); + + // 3. Advance the exchange (save.getWeek() - 1) times so the + // stock price graphs get a real history, and so widgets that + // track per-week net-worth (the financial summary and the + // stats screen) accumulate chart points via their existing + // weekProperty listeners. + int targetWeek = Math.max(1, save.getWeek()); + while (exchange.getWeek() < targetWeek) { + exchange.advance(); + } + + // 4. Re-apply the saved owned shares (skipping unknown symbols + // instead of throwing - keeps the load robust against stock + // files that have diverged from the save). + for (OwnedShareData od : save.getOwnedShares()) { + if (!exchange.hasStock(od.getSymbol())) { + System.err.println("Skipping unknown stock from save: " + + od.getSymbol()); + continue; + } + Stock stock = exchange.getStock(od.getSymbol()); + portfolio.addShare(new Share( + stock, od.getQuantity(), od.getPurchasePrice())); + } + + // 5. Re-apply the saved transactions. We rebuild the original + // Purchase / Sale objects (with their share + calculator) + // directly into the archive, rather than re-running them + // through the player's transaction handler, so the player's + // balance is not mutated again here. + for (TransactionData td : save.getTransactions()) { + if (!exchange.hasStock(td.getSymbol())) { + System.err.println("Skipping transaction with unknown stock: " + + td.getSymbol()); + continue; + } + Stock stock = exchange.getStock(td.getSymbol()); + Share share = new Share(stock, td.getQuantity(), td.getPrice()); + TransactionCalculator calculator = (td.getType() == TransactionType.SALE) + ? new SaleCalculator(share) + : new PurchaseCalculator(share); + Transaction transaction = (td.getType() == TransactionType.SALE) + ? new Sale(share, td.getWeek(), calculator) + : new Purchase(share, td.getWeek(), calculator); + archive.add(transaction); + } + + // 6. Overwrite the balance with the saved value now that the + // history has been built up. + player.setMoney(BigDecimal.valueOf(save.getBalance())); + + // 7. Re-publish the float properties so the final loaded + // balance is visible to all listeners. + player.refreshProperties(); + + this.activeSave = save; + } + + /** + * Captures the current sales price of every stock provided, so saves + * can be loaded against a stable baseline regardless of subsequent + * price drift during the session. + * */ + private static Map captureBaseline(final List stocks) { + Map snapshot = new HashMap<>(); + for (Stock s : stocks) { + snapshot.put(s.getSymbol(), s.getSalesPrice()); + } + return snapshot; + } +} diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/SaveGameService.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/SaveGameService.java index 2c6aaed..aed5026 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/SaveGameService.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/SaveGameService.java @@ -1,6 +1,8 @@ package edu.ntnu.idi.idatt2003.g40.mappe.service; +import edu.ntnu.idi.idatt2003.g40.mappe.model.OwnedShareData; import edu.ntnu.idi.idatt2003.g40.mappe.model.SaveGame; +import edu.ntnu.idi.idatt2003.g40.mappe.model.TransactionData; import java.io.IOException; import java.math.BigDecimal; @@ -9,6 +11,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; +import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; @@ -30,16 +33,31 @@ * "name": "MySave", * "balance": 10000.00, * "startingCapital": 10000.00, - * "stockDataPath": null + * "stockDataPath": null, + * "week": 1, + * "ownedShares": [ + * { "symbol": "AAPL", "quantity": 5, "purchasePrice": 150.00 } + * ], + * "transactions": [ + * { "type": "PURCHASE", "symbol": "AAPL", "quantity": 5, + * "price": 150.00, "week": 1 } + * ] * } * * *

+ * The {@code week}, {@code ownedShares} and {@code transactions} + * fields are optional - old saves without them load with sensible + * defaults (week 1, empty lists). + *

+ * + *

* The service exposes both a "load all saves" entry point (used by * {@link edu.ntnu.idi.idatt2003.g40.mappe.view.playgame.PlayGameView}) - * and a "save one" entry point (used by the create-game flow). The - * JSON parsing logic is intentionally minimal and tailored to this - * format — invalid files are skipped rather than throwing. + * and a "save one" entry point (used by the create-game flow and the + * auto-save flow). The JSON parsing logic is intentionally minimal and + * tailored to this format - invalid files are skipped rather than + * throwing. *

*/ public class SaveGameService { @@ -188,7 +206,9 @@ public SaveGame loadSaveFromFile(final Path file) { * *

* Returns {@code null} for files that don't contain a valid save - * record (missing required fields or malformed JSON). + * record (missing required fields or malformed JSON). Old saves + * that don't have week / ownedShares / transactions still parse + * cleanly, with those fields defaulted (week 1, empty lists). *

* * @param file path to the file to parse. @@ -219,7 +239,25 @@ private SaveGame parseFile(final Path file) { stockDataPath = null; } - return new SaveGame(name, balance, startingCapital, stockDataPath); + // Week - defaults to 1 for older saves that didn't store it. + int week = 1; + if (fields.containsKey("week") && !fields.get("week").isEmpty()) { + week = Integer.parseInt(fields.get("week")); + if (week < 1) { + week = 1; + } + } + + // The flat parser can't represent arrays in the result map, so + // the array bodies are pulled straight out of the raw content + // with extractArrayBody and then parsed object-by-object. + List ownedShares = + parseOwnedSharesArray(extractArrayBody(content, "ownedShares")); + List transactions = + parseTransactionsArray(extractArrayBody(content, "transactions")); + + return new SaveGame(name, balance, startingCapital, stockDataPath, + week, ownedShares, transactions); } catch (IOException | NumberFormatException e) { System.err.println("Skipping invalid save file " + file.getFileName() + ": " + e.getMessage()); @@ -234,7 +272,8 @@ private SaveGame parseFile(final Path file) { * Output is pretty-printed with two-space indentation and a trailing * newline so the files are human-readable. Numeric values are * written via {@link BigDecimal#toPlainString()} so they never - * surface as scientific notation. + * surface as scientific notation. Owned shares and transactions + * write as JSON arrays of small flat objects. *

* * @param save the save to convert. @@ -254,11 +293,78 @@ private String toJson(final SaveGame save) { } else { sb.append(quote(save.getStockDataPath())); } - sb.append("\n"); + sb.append(",\n"); + sb.append(" \"week\": ").append(save.getWeek()).append(",\n"); + sb.append(" \"ownedShares\": ") + .append(ownedSharesToJson(save.getOwnedShares())).append(",\n"); + sb.append(" \"transactions\": ") + .append(transactionsToJson(save.getTransactions())).append("\n"); sb.append("}\n"); return sb.toString(); } + /** + * Serialises a list of {@link OwnedShareData} entries to a JSON array. + * + *

Returns {@code "[]"} for empty lists, and a pretty-printed + * array for non-empty lists - one object per line, indented to + * four spaces.

+ */ + private String ownedSharesToJson(final List shares) { + if (shares == null || shares.isEmpty()) { + return "[]"; + } + StringBuilder sb = new StringBuilder(); + sb.append("[\n"); + for (int i = 0; i < shares.size(); i++) { + OwnedShareData s = shares.get(i); + sb.append(" { \"symbol\": ").append(quote(s.getSymbol())) + .append(", \"quantity\": ") + .append(s.getQuantity().toPlainString()) + .append(", \"purchasePrice\": ") + .append(s.getPurchasePrice().toPlainString()) + .append(" }"); + if (i < shares.size() - 1) { + sb.append(","); + } + sb.append("\n"); + } + sb.append(" ]"); + return sb.toString(); + } + + /** + * Serialises a list of {@link TransactionData} entries to a JSON array. + * + *

Returns {@code "[]"} for empty lists, and a pretty-printed + * array for non-empty lists - one object per line, indented to + * four spaces.

+ */ + private String transactionsToJson(final List txns) { + if (txns == null || txns.isEmpty()) { + return "[]"; + } + StringBuilder sb = new StringBuilder(); + sb.append("[\n"); + for (int i = 0; i < txns.size(); i++) { + TransactionData t = txns.get(i); + sb.append(" { \"type\": ").append(quote(t.getType().name())) + .append(", \"symbol\": ").append(quote(t.getSymbol())) + .append(", \"quantity\": ") + .append(t.getQuantity().toPlainString()) + .append(", \"price\": ") + .append(t.getPrice().toPlainString()) + .append(", \"week\": ").append(t.getWeek()) + .append(" }"); + if (i < txns.size() - 1) { + sb.append(","); + } + sb.append("\n"); + } + sb.append(" ]"); + return sb.toString(); + } + /** * Formats a double as a plain (never scientific) decimal string. * Uses {@link BigDecimal#valueOf(double)} for the conversion so the @@ -268,13 +374,234 @@ private String formatNumber(final double value) { return BigDecimal.valueOf(value).toPlainString(); } + /** + * Extracts the body of a top-level JSON array field from a raw save + * file's content. + * + *

+ * Given the raw content {@code {"name": "x", "ownedShares": [ ... ]}} + * and a field name {@code "ownedShares"}, returns the substring + * between the opening {@code [} and the matching closing {@code ]}. + *

+ * + *

+ * Returns {@code null} if the field is not present, or if the array + * is malformed (no closing bracket). Returns an empty string for an + * empty array. + *

+ * + *

+ * Brackets inside quoted strings are ignored, so a string value like + * {@code "weird]name"} won't confuse the matcher. + *

+ * + * @param content the raw file content. + * @param fieldName the JSON field name to look for. + * @return the body of the matching array, or {@code null}. + * */ + private String extractArrayBody(final String content, + final String fieldName) { + if (content == null) { + return null; + } + String needle = "\"" + fieldName + "\""; + int keyIdx = content.indexOf(needle); + if (keyIdx < 0) { + return null; + } + int colonIdx = content.indexOf(':', keyIdx + needle.length()); + if (colonIdx < 0) { + return null; + } + int openIdx = colonIdx + 1; + while (openIdx < content.length() + && Character.isWhitespace(content.charAt(openIdx))) { + openIdx++; + } + if (openIdx >= content.length() || content.charAt(openIdx) != '[') { + return null; + } + + int depth = 1; + boolean inString = false; + int i = openIdx + 1; + while (i < content.length()) { + char c = content.charAt(i); + if (inString) { + if (c == '\\' && i + 1 < content.length()) { + i += 2; + continue; + } + if (c == '"') { + inString = false; + } + } else { + if (c == '"') { + inString = true; + } else if (c == '[') { + depth++; + } else if (c == ']') { + depth--; + if (depth == 0) { + return content.substring(openIdx + 1, i); + } + } + } + i++; + } + return null; + } + + /** + * Splits a JSON array body into the substrings of its inner objects. + * + *

+ * Tracks bracket and quote nesting so that commas inside the inner + * objects (or inside quoted strings) don't split the body + * incorrectly. Returns the substrings of each top-level + * {@code {...}} found. + *

+ * + * @param body the substring between {@code [} and {@code ]}. + * @return list of object substrings, or an empty list. + * */ + private List splitArrayObjects(final String body) { + List objects = new ArrayList<>(); + if (body == null || body.trim().isEmpty()) { + return objects; + } + int depth = 0; + boolean inString = false; + int objectStart = -1; + int i = 0; + while (i < body.length()) { + char c = body.charAt(i); + if (inString) { + if (c == '\\' && i + 1 < body.length()) { + i += 2; + continue; + } + if (c == '"') { + inString = false; + } + } else { + if (c == '"') { + inString = true; + } else if (c == '{') { + if (depth == 0) { + objectStart = i; + } + depth++; + } else if (c == '}') { + depth--; + if (depth == 0 && objectStart >= 0) { + objects.add(body.substring(objectStart, i + 1)); + objectStart = -1; + } + } + } + i++; + } + return objects; + } + + /** + * Parses the body of an {@code ownedShares} array into a list of + * {@link OwnedShareData}. + * + *

Returns an empty list for a {@code null}/empty body. Objects + * that fail to parse are silently skipped so a single malformed + * entry doesn't break the rest of the save.

+ * + * @param body the raw substring between {@code [} and {@code ]}. + * @return parsed list, never {@code null}. + * */ + private List parseOwnedSharesArray(final String body) { + List result = new ArrayList<>(); + if (body == null) { + return Collections.emptyList(); + } + for (String objectStr : splitArrayObjects(body)) { + Map fields = parseFlatJsonObject(objectStr); + if (fields == null) { + continue; + } + String symbol = fields.get("symbol"); + String quantityStr = fields.get("quantity"); + String priceStr = fields.get("purchasePrice"); + if (symbol == null || quantityStr == null || priceStr == null) { + continue; + } + try { + result.add(new OwnedShareData( + symbol, + new BigDecimal(quantityStr), + new BigDecimal(priceStr))); + } catch (IllegalArgumentException e) { + System.err.println("Skipping malformed owned share entry: " + + e.getMessage()); + } + } + return result; + } + + /** + * Parses the body of a {@code transactions} array into a list of + * {@link TransactionData}. + * + *

Returns an empty list for a {@code null}/empty body. Objects + * that fail to parse are silently skipped so a single malformed + * entry doesn't break the rest of the save.

+ * + * @param body the raw substring between {@code [} and {@code ]}. + * @return parsed list, never {@code null}. + * */ + private List parseTransactionsArray(final String body) { + List result = new ArrayList<>(); + if (body == null) { + return Collections.emptyList(); + } + for (String objectStr : splitArrayObjects(body)) { + Map fields = parseFlatJsonObject(objectStr); + if (fields == null) { + continue; + } + String typeStr = fields.get("type"); + String symbol = fields.get("symbol"); + String quantityStr = fields.get("quantity"); + String priceStr = fields.get("price"); + String weekStr = fields.get("week"); + if (typeStr == null || symbol == null + || quantityStr == null || priceStr == null + || weekStr == null) { + continue; + } + try { + result.add(new TransactionData( + TransactionType.valueOf(typeStr), + symbol, + new BigDecimal(quantityStr), + new BigDecimal(priceStr), + Integer.parseInt(weekStr))); + } catch (IllegalArgumentException e) { + System.err.println("Skipping malformed transaction entry: " + + e.getMessage()); + } + } + return result; + } + /** * Parses a flat (one level deep) JSON object into a map. * *

- * Only supports string, number, boolean and null values, which is - * everything {@link SaveGame} needs. Returns {@code null} if the - * content can't be parsed. + * Only supports string, number, boolean and null values for the + * top-level entries (which is everything {@link SaveGame} and + * {@link OwnedShareData} / {@link TransactionData} need). Nested + * arrays and objects are skipped past with an empty-string value, + * since the array contents are read separately via + * {@link #extractArrayBody}. Returns {@code null} if the content + * can't be parsed. *

* *

@@ -335,6 +662,17 @@ private Map parseFlatJsonObject(final String content) { } value = unquote(body.substring(i, valEnd + 1)); i = valEnd + 1; + } else if (body.charAt(i) == '[' || body.charAt(i) == '{') { + // Nested array or object - we don't need its content at this + // level (top-level arrays are pulled out via extractArrayBody), + // so skip past it while tracking bracket / brace nesting and + // string boundaries, and record the field with an empty value. + int valEnd = findStructuredEnd(body, i); + if (valEnd < 0) { + return null; + } + value = ""; + i = valEnd + 1; } else { int valEnd = i; while (valEnd < body.length() @@ -391,6 +729,57 @@ private int findStringEnd(final String s, final int start) { return -1; } + /** + * Finds the index of the closing bracket / brace that matches the + * opening bracket / brace at {@code start}, accounting for quoted + * strings and nested structures. Returns -1 if no match is found. + * + *

+ * Used by {@link #parseFlatJsonObject} to skip past nested arrays + * and objects whose content isn't needed at the top level (top-level + * arrays are read separately by {@link #extractArrayBody}). + *

+ * */ + private int findStructuredEnd(final String s, final int start) { + char open = s.charAt(start); + char close; + if (open == '[') { + close = ']'; + } else if (open == '{') { + close = '}'; + } else { + return -1; + } + int depth = 1; + boolean inString = false; + int i = start + 1; + while (i < s.length()) { + char c = s.charAt(i); + if (inString) { + if (c == '\\' && i + 1 < s.length()) { + i += 2; + continue; + } + if (c == '"') { + inString = false; + } + } else { + if (c == '"') { + inString = true; + } else if (c == open) { + depth++; + } else if (c == close) { + depth--; + if (depth == 0) { + return i; + } + } + } + i++; + } + return -1; + } + /** * Wraps a string in double quotes and escapes characters that * need to be escaped in JSON. diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/event/EventType.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/event/EventType.java index ace4933..47aba89 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/event/EventType.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/event/EventType.java @@ -50,7 +50,47 @@ public enum EventType implements EventChannel { * @see edu.ntnu.idi.idatt2003.g40.mappe.view.widgets.dashboard.DashBoardController * */ - SELECT_STOCK_FOR_MINIGAME; + SELECT_STOCK_FOR_MINIGAME, + + /** + * Event type representing a request to load a + * {@link edu.ntnu.idi.idatt2003.g40.mappe.model.SaveGame} into the + * active player and exchange. + * + *

The payload is the {@link edu.ntnu.idi.idatt2003.g40.mappe.model.SaveGame} + * to apply. Handled by + * {@link edu.ntnu.idi.idatt2003.g40.mappe.service.GameStateLoader}, + * which mutates the live player and exchange to match the save and + * then publishes a {@link #STATE_RESET} event so widgets refresh.

+ * */ + LOAD_SAVE, + + /** + * Event type representing that a {@link + * edu.ntnu.idi.idatt2003.g40.mappe.model.SaveGame} is about to be + * loaded, fired by + * {@link edu.ntnu.idi.idatt2003.g40.mappe.service.GameStateLoader} + * before it begins mutating state and replaying simulation weeks. + * + *

Widget controllers that accumulate per-week history (chart + * data) listen for this event and clear that history, so the + * advance-replay during the load can rebuild it cleanly without + * mixing samples from the previous session.

+ * */ + PRE_LOAD_SAVE, + + /** + * Event type representing that the player and exchange state has + * just been reset (typically by loading a save). + * + *

Widget controllers that maintain their own derived state + * (chart history, transaction-week selectors, etc.) listen to this + * event so they can rebuild that state from the fresh player / + * exchange. The payload is the {@link + * edu.ntnu.idi.idatt2003.g40.mappe.model.SaveGame} that was loaded + * but most listeners don't inspect it.

+ * */ + STATE_RESET; /** * {@inheritDoc} diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/playgame/PlayGameController.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/playgame/PlayGameController.java index de5e565..b5001f3 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/playgame/PlayGameController.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/playgame/PlayGameController.java @@ -2,7 +2,9 @@ import edu.ntnu.idi.idatt2003.g40.mappe.model.SaveGame; import edu.ntnu.idi.idatt2003.g40.mappe.service.SaveGameService; +import edu.ntnu.idi.idatt2003.g40.mappe.service.event.EventData; import edu.ntnu.idi.idatt2003.g40.mappe.service.event.EventManager; +import edu.ntnu.idi.idatt2003.g40.mappe.service.event.EventType; import edu.ntnu.idi.idatt2003.g40.mappe.view.ViewController; import edu.ntnu.idi.idatt2003.g40.mappe.view.ViewEnum; @@ -94,8 +96,52 @@ protected void initInteractions() { getViewElement().setOnAction(PlayGameActions.UPLOAD_SAVE, this::handleUploadSave); - getViewElement().setOnSaveSelected(save -> - changeScene(ViewEnum.IN_GAME)); + getViewElement().setOnSaveSelected(save -> { + // The SaveGame instance bound to the row is from the last time + // refresh() ran (typically at app start or right after a save + // was created). The auto-save flow on quit writes new content + // to disk after that, so the in-memory copy is stale. Re-read + // the save from disk by name and use that instead. + SaveGame fresh = findFreshSaveByName(save.getName()); + SaveGame toLoad = (fresh != null) ? fresh : save; + + // Apply the chosen save to the live player + exchange before + // navigating to the in-game view. The GameStateLoader handles + // the mutation and then publishes STATE_RESET so each widget + // refreshes its derived state. + invoke(new EventData<>(EventType.LOAD_SAVE, toLoad)); + changeScene(ViewEnum.IN_GAME); + + // Refresh the in-memory save list as well so the row shown in + // the play-game view matches what's now on disk. + refresh(); + }); + } + + /** + * Re-reads every save from disk and returns the one whose name + * matches, or {@code null} if no matching save is found. + * + *

+ * Used to defeat the staleness of the {@link SaveGame} instance + * bound to the clicked row in {@link PlayGameView}: between when + * {@link #refresh()} last ran and the click, the auto-save flow may + * have written newer content to disk. + *

+ * + * @param name the save display name to look up. + * @return the freshly parsed {@link SaveGame}, or {@code null}. + * */ + private SaveGame findFreshSaveByName(final String name) { + if (name == null) { + return null; + } + for (SaveGame candidate : saveGameService.loadSaves()) { + if (name.equals(candidate.getName())) { + return candidate; + } + } + return null; } /** diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/dashboard/DashBoardController.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/dashboard/DashBoardController.java index 9d31d0d..fdc7182 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/dashboard/DashBoardController.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/dashboard/DashBoardController.java @@ -4,6 +4,7 @@ import edu.ntnu.idi.idatt2003.g40.mappe.model.*; import edu.ntnu.idi.idatt2003.g40.mappe.service.event.EventData; import edu.ntnu.idi.idatt2003.g40.mappe.service.event.EventManager; +import edu.ntnu.idi.idatt2003.g40.mappe.service.event.EventSubscriber; import edu.ntnu.idi.idatt2003.g40.mappe.service.event.EventType; import edu.ntnu.idi.idatt2003.g40.mappe.utils.Validator; import edu.ntnu.idi.idatt2003.g40.mappe.view.ViewController; @@ -17,7 +18,8 @@ /** * Controller for {@link DashBoardView}. * */ -public class DashBoardController extends ViewController { +public class DashBoardController extends ViewController + implements EventSubscriber { /** * This player instance. @@ -61,6 +63,7 @@ public DashBoardController(final DashBoardView viewElement, this.selectedFilter = ""; this.selectedTimeRange = DashBoardTimeRange.DEFAULT; super(viewElement, eventManager); + eventManager.addSubscriber(this, EventType.STATE_RESET); } /** @@ -217,4 +220,25 @@ protected void initInteractions() { populateStockList(selectedFilter); }); } + + /** + * Refresh the dashboard view after a save has been loaded. + * + *

Rebuilds the stock list (so the "owned" counts shown beside + * each stock reflect the newly loaded portfolio), re-selects the + * first stock as the current focus, and redraws the price graph.

+ * */ + @Override + public void handleEvent(final EventData data) { + System.out.println("[dashboard] STATE_RESET: shares in portfolio = " + + player.getPortfolio().getShares().size()); + populateStockList(selectedFilter); + if (!stockList.isEmpty()) { + Stock first = stockList.getFirst(); + BigDecimal owned = player.getPortfolio() + .getTotalSharesBySymbol(first.getSymbol()); + getViewElement().setCurrentStock(first, owned.floatValue()); + } + getViewElement().updateGraph(selectedTimeRange); + } } diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/financialsummary/SummaryController.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/financialsummary/SummaryController.java index ee3b88e..d4234e5 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/financialsummary/SummaryController.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/financialsummary/SummaryController.java @@ -2,13 +2,16 @@ import edu.ntnu.idi.idatt2003.g40.mappe.engine.Exchange; import edu.ntnu.idi.idatt2003.g40.mappe.model.Player; +import edu.ntnu.idi.idatt2003.g40.mappe.service.event.EventData; import edu.ntnu.idi.idatt2003.g40.mappe.service.event.EventManager; +import edu.ntnu.idi.idatt2003.g40.mappe.service.event.EventSubscriber; +import edu.ntnu.idi.idatt2003.g40.mappe.service.event.EventType; import edu.ntnu.idi.idatt2003.g40.mappe.view.ViewController; -import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; -public class SummaryController extends ViewController { +public class SummaryController extends ViewController + implements EventSubscriber { private Exchange exchange; private Player player; @@ -26,6 +29,8 @@ public SummaryController(final SummaryView viewElement, this.player = player; this.playerNetWorthHistory = new ArrayList<>(); super(viewElement, eventManager); + eventManager.addSubscriber(this, EventType.STATE_RESET); + eventManager.addSubscriber(this, EventType.PRE_LOAD_SAVE); getViewElement().setBalance(player.getStartingMoney().floatValue(), player.getStartingMoney().floatValue()); } @@ -51,4 +56,48 @@ protected void initInteractions() { getViewElement().setBalance(player.getMoney().floatValue(), player.getNetWorth().floatValue()); }); } + + /** + * Handles save-related events. + * + *

On {@link EventType#PRE_LOAD_SAVE} the chart history is + * cleared so the advance-replay during the load can rebuild it + * cleanly. On {@link EventType#STATE_RESET} the final balance + * point is replaced with the actual loaded net worth, and the + * week / balance labels are pushed into the view.

+ * */ + @Override + public void handleEvent(final EventData data) { + if (data.channel() == EventType.PRE_LOAD_SAVE) { + if (playerNetWorthHistory == null) { + playerNetWorthHistory = new ArrayList<>(); + } + playerNetWorthHistory.clear(); + return; + } + // STATE_RESET + System.out.println("[summary] STATE_RESET: money=" + player.getMoney() + + ", netWorth=" + player.getNetWorth() + + ", week=" + exchange.getWeek() + + ", history size=" + (playerNetWorthHistory == null ? 0 : playerNetWorthHistory.size())); + if (playerNetWorthHistory == null) { + playerNetWorthHistory = new ArrayList<>(); + } + // The advance-replay during the load has populated history with + // net-worth samples computed against the placeholder starting + // capital. The final point should reflect the actual loaded + // balance, so we overwrite it (or add it if the history is + // empty - e.g. for a week-1 save with no replay). + if (!playerNetWorthHistory.isEmpty()) { + playerNetWorthHistory.set( + playerNetWorthHistory.size() - 1, + player.getNetWorth().floatValue()); + } else { + playerNetWorthHistory.add(player.getNetWorth().floatValue()); + } + getViewElement().setWeek(exchange.getWeek()); + getViewElement().updateChart(playerNetWorthHistory); + getViewElement().setBalance(player.getMoney().floatValue(), + player.getNetWorth().floatValue()); + } } diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/market/MarketController.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/market/MarketController.java index e185fb3..d8721cf 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/market/MarketController.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/market/MarketController.java @@ -4,7 +4,10 @@ import edu.ntnu.idi.idatt2003.g40.mappe.model.Player; import edu.ntnu.idi.idatt2003.g40.mappe.model.Share; import edu.ntnu.idi.idatt2003.g40.mappe.model.Stock; +import edu.ntnu.idi.idatt2003.g40.mappe.service.event.EventData; import edu.ntnu.idi.idatt2003.g40.mappe.service.event.EventManager; +import edu.ntnu.idi.idatt2003.g40.mappe.service.event.EventSubscriber; +import edu.ntnu.idi.idatt2003.g40.mappe.service.event.EventType; import edu.ntnu.idi.idatt2003.g40.mappe.utils.Validator; import edu.ntnu.idi.idatt2003.g40.mappe.view.ViewController; @@ -18,7 +21,8 @@ * {@link Player} net worth so the grid (prices, change %, owned counts) * stays in sync as the game advances.

* */ -public class MarketController extends ViewController { +public class MarketController extends ViewController + implements EventSubscriber { /** The {@link Player} owning shares displayed in the market. */ private final Player player; @@ -50,6 +54,7 @@ public MarketController(final MarketView viewElement, this.exchange = exchange; this.stockList = stockList; super(viewElement, eventManager); + eventManager.addSubscriber(this, EventType.STATE_RESET); } /** {@inheritDoc} */ @@ -76,6 +81,21 @@ protected void initInteractions() { }); } + /** + * Refresh the market grid after a save has been loaded. + * + *

+ * The week-property and net-worth-property listeners will trigger + * renderStocks() when the save loader mutates state, but we also + * re-render here explicitly so the grid is correct even if the new + * state happens to match the old listener values. + *

+ * */ + @Override + public void handleEvent(final EventData data) { + getViewElement().renderStocks(); + } + /** * Returns the total quantity of shares the player owns of a given symbol. * diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/stats/StatsController.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/stats/StatsController.java index 512a026..d2a7316 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/stats/StatsController.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/stats/StatsController.java @@ -4,7 +4,10 @@ import edu.ntnu.idi.idatt2003.g40.mappe.model.Player; import edu.ntnu.idi.idatt2003.g40.mappe.model.Share; import edu.ntnu.idi.idatt2003.g40.mappe.model.Stock; +import edu.ntnu.idi.idatt2003.g40.mappe.service.event.EventData; import edu.ntnu.idi.idatt2003.g40.mappe.service.event.EventManager; +import edu.ntnu.idi.idatt2003.g40.mappe.service.event.EventSubscriber; +import edu.ntnu.idi.idatt2003.g40.mappe.service.event.EventType; import edu.ntnu.idi.idatt2003.g40.mappe.view.ViewController; import java.math.BigDecimal; @@ -30,7 +33,8 @@ * single row with the weighted average purchase price. *

*/ -public class StatsController extends ViewController { +public class StatsController extends ViewController + implements EventSubscriber { /** The {@link Player} whose state is displayed. */ private final Player player; @@ -69,6 +73,8 @@ public StatsController(final StatsView viewElement, this.player = player; this.exchange = exchange; super(viewElement, eventManager); + eventManager.addSubscriber(this, EventType.STATE_RESET); + eventManager.addSubscriber(this, EventType.PRE_LOAD_SAVE); } /** {@inheritDoc} */ @@ -95,6 +101,39 @@ protected void initInteractions() { }); } + /** + * Handles save-related events. + * + *

On {@link EventType#PRE_LOAD_SAVE} the balance history is + * cleared so the advance-replay during the load can rebuild it + * cleanly. On {@link EventType#STATE_RESET} the final entry is + * overwritten with the actual loaded net worth, then a snapshot + * is pushed.

+ * */ + @Override + public void handleEvent(final EventData data) { + if (balanceHistory == null) { + balanceHistory = new ArrayList<>(); + } + if (data.channel() == EventType.PRE_LOAD_SAVE) { + balanceHistory.clear(); + return; + } + // STATE_RESET: the advance-replay populated the history against + // the placeholder starting capital. Replace the final entry + // with the actual loaded net worth so the chart's endpoint + // matches the displayed balance. + if (!balanceHistory.isEmpty()) { + balanceHistory.set( + balanceHistory.size() - 1, + player.getNetWorth()); + } else { + balanceHistory.add(player.getStartingMoney()); + balanceHistory.add(player.getNetWorth()); + } + pushSnapshot(); + } + /** * Pushes the current player/exchange state into the view. */ diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/topbar/TopBarController.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/topbar/TopBarController.java index a785cd3..e338eb6 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/topbar/TopBarController.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/topbar/TopBarController.java @@ -49,6 +49,16 @@ public class TopBarController extends ViewController { */ private boolean inMinigamesView = false; + /** + * Optional hook invoked just before the in-game session is left + * (when the quit/back button returns to the main menu). + * + *

Used by the application to auto-save the active save before + * the player goes back to the play-game list. Defaults to a no-op + * so the controller stays usable when no hook is installed.

+ * */ + private Runnable onQuitToMainMenu = () -> { }; + /** * {@inheritDoc}. @@ -60,7 +70,10 @@ public TopBarController(final TopBarView viewElement, final EventManager eventMa @Override protected void initInteractions() { - getViewElement().setOnAction(TopBarActions.EXIT, () -> changeScene(ViewEnum.MAIN_MENU)); + getViewElement().setOnAction(TopBarActions.EXIT, () -> { + onQuitToMainMenu.run(); + changeScene(ViewEnum.MAIN_MENU); + }); getViewElement().setOnAction(TopBarActions.STATS, () -> changeScene(ViewEnum.MAIN_MENU)); @@ -116,6 +129,7 @@ public void setMarketIntegration(final Consumer centerSwitcher, inTransactionsView = false; inMinigamesView = false; } else { + onQuitToMainMenu.run(); changeScene(ViewEnum.MAIN_MENU); } }); @@ -171,4 +185,18 @@ public void setMarketIntegration(final Consumer centerSwitcher, public void setSettingsAction(final Runnable onSettings) { getViewElement().setOnAction(TopBarActions.SETTINGS, onSettings); } + + /** + * Sets the hook invoked when the player leaves the in-game session + * back to the main menu. + * + *

Used by the application to auto-save the active save before + * the player returns to the play-game list, so progress made in + * this session is persisted without an explicit save action.

+ * + * @param onQuit runnable invoked just before the scene change. + * */ + public void setOnQuitToMainMenu(final Runnable onQuit) { + this.onQuitToMainMenu = (onQuit != null) ? onQuit : () -> { }; + } } \ No newline at end of file diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/transactions/TransactionsController.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/transactions/TransactionsController.java index 7179630..a034fdb 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/transactions/TransactionsController.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/transactions/TransactionsController.java @@ -3,7 +3,10 @@ import edu.ntnu.idi.idatt2003.g40.mappe.engine.TransactionArchive; import edu.ntnu.idi.idatt2003.g40.mappe.model.Sale; import edu.ntnu.idi.idatt2003.g40.mappe.model.Transaction; +import edu.ntnu.idi.idatt2003.g40.mappe.service.event.EventData; import edu.ntnu.idi.idatt2003.g40.mappe.service.event.EventManager; +import edu.ntnu.idi.idatt2003.g40.mappe.service.event.EventSubscriber; +import edu.ntnu.idi.idatt2003.g40.mappe.service.event.EventType; import edu.ntnu.idi.idatt2003.g40.mappe.view.ViewController; import java.util.List; @@ -12,7 +15,8 @@ * *

extends {@link ViewController}

* */ -public class TransactionsController extends ViewController { +public class TransactionsController extends ViewController + implements EventSubscriber { private final TransactionArchive transactionArchive; public TransactionsController(final TransactionsView viewElement, @@ -20,6 +24,7 @@ public TransactionsController(final TransactionsView viewElement, final TransactionArchive transactionArchive) { this.transactionArchive = transactionArchive; super(viewElement, eventManager); + eventManager.addSubscriber(this, EventType.STATE_RESET); getViewElement().clearCards(); } @@ -85,15 +90,54 @@ public void refresh() { List activeWeeks = getUniqueTransactionWeeks(transactionArchive.getTransactions()); getViewElement().setWeekSelectBoxOptions(activeWeeks); - if (activeWeeks.contains(Integer.parseInt(getViewElement().getWeekSelectBox().getValue()))) { - getViewElement().getWeekSelectBox().setValue(getViewElement().getWeekSelectBox().getValue()); + // The select box can be empty (no value chosen yet, or the archive + // is empty for this save) - guard so we don't NPE/parse-fail. + Integer currentSelection = parseSelectedWeek(); + + if (currentSelection != null && activeWeeks.contains(currentSelection)) { + getViewElement().getWeekSelectBox().setValue(currentSelection.toString()); } else if (!activeWeeks.isEmpty()) { getViewElement().getWeekSelectBox().setValue(activeWeeks.getFirst().toString()); + } else { + // Nothing to show. + getViewElement().clearCards(); + return; + } + + Integer targetWeek = parseSelectedWeek(); + if (targetWeek == null) { + getViewElement().clearCards(); + return; + } + filterData(getViewElement().getSearchField().getText(), targetWeek); + } + + /** + * Reads the current value of the week selector and parses it as an + * integer, returning {@code null} if the value is missing or not a + * valid number. + * */ + private Integer parseSelectedWeek() { + String raw = getViewElement().getWeekSelectBox().getValue(); + if (raw == null || raw.isBlank()) { + return null; + } + try { + return Integer.parseInt(raw); + } catch (NumberFormatException e) { + return null; } + } - filterData( - getViewElement().getSearchField().getText(), - Integer.parseInt(getViewElement().getWeekSelectBox().getValue()) - ); + /** + * Refresh the transactions view after a save has been loaded. + * + *

Delegates to {@link #refresh()} which rebuilds the week selector + * options from the freshly loaded archive and re-renders the cards + * for the current selection.

+ * */ + @Override + public void handleEvent(final EventData data) { + refresh(); } } \ No newline at end of file diff --git a/src/main/resources/saves/Newbie.json b/src/main/resources/saves/Newbie.json index 893047a..823acaf 100644 --- a/src/main/resources/saves/Newbie.json +++ b/src/main/resources/saves/Newbie.json @@ -1,6 +1,31 @@ { "name": "Newbie", - "balance": 10000.0, + "balance": 388.6825, "startingCapital": 10000.0, - "stockDataPath": null + "stockDataPath": null, + "week": 5, + "ownedShares": [ + { "symbol": "NVDA", "quantity": 5.0, "purchasePrice": 191.27 }, + { "symbol": "NVDA", "quantity": 5.0, "purchasePrice": 191.27 }, + { "symbol": "NVDA", "quantity": 5.0, "purchasePrice": 191.27 }, + { "symbol": "NVDA", "quantity": 5.0, "purchasePrice": 191.27 }, + { "symbol": "NVDA", "quantity": 5.0, "purchasePrice": 191.27 }, + { "symbol": "NVDA", "quantity": 5.0, "purchasePrice": 191.27 }, + { "symbol": "NVDA", "quantity": 5.0, "purchasePrice": 191.27 }, + { "symbol": "NVDA", "quantity": 5.0, "purchasePrice": 191.27 }, + { "symbol": "NVDA", "quantity": 5.0, "purchasePrice": 191.27 }, + { "symbol": "NVDA", "quantity": 5.0, "purchasePrice": 191.27 } + ], + "transactions": [ + { "type": "PURCHASE", "symbol": "NVDA", "quantity": 5.0, "price": 191.27, "week": 1 }, + { "type": "PURCHASE", "symbol": "NVDA", "quantity": 5.0, "price": 191.27, "week": 1 }, + { "type": "PURCHASE", "symbol": "NVDA", "quantity": 5.0, "price": 191.27, "week": 1 }, + { "type": "PURCHASE", "symbol": "NVDA", "quantity": 5.0, "price": 191.27, "week": 1 }, + { "type": "PURCHASE", "symbol": "NVDA", "quantity": 5.0, "price": 191.27, "week": 1 }, + { "type": "PURCHASE", "symbol": "NVDA", "quantity": 5.0, "price": 191.27, "week": 1 }, + { "type": "PURCHASE", "symbol": "NVDA", "quantity": 5.0, "price": 191.27, "week": 1 }, + { "type": "PURCHASE", "symbol": "NVDA", "quantity": 5.0, "price": 191.27, "week": 1 }, + { "type": "PURCHASE", "symbol": "NVDA", "quantity": 5.0, "price": 191.27, "week": 1 }, + { "type": "PURCHASE", "symbol": "NVDA", "quantity": 5.0, "price": 191.27, "week": 1 } + ] } From 8bc2c8b431aad4c3c295a77e2600a7cd44012b62 Mon Sep 17 00:00:00 2001 From: = Date: Mon, 25 May 2026 23:46:34 +0200 Subject: [PATCH 2/2] Fix: Fixed merge conflict --- .../ntnu/idi/idatt2003/g40/mappe/Main.java | 10 ++--- .../idatt2003/g40/mappe/model/SaveGame.java | 7 +++ .../g40/mappe/service/event/EventType.java | 8 ---- .../dashboard/DashBoardController.java | 2 +- src/main/resources/saves/Tommy.json | 43 +++++++++++++++++++ 5 files changed, 56 insertions(+), 14 deletions(-) create mode 100644 src/main/resources/saves/Tommy.json diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/Main.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/Main.java index cec044e..52f90fd 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/Main.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/Main.java @@ -4,10 +4,10 @@ import edu.ntnu.idi.idatt2003.g40.mappe.model.Player; import edu.ntnu.idi.idatt2003.g40.mappe.model.SaveGame; import edu.ntnu.idi.idatt2003.g40.mappe.model.Stock; -import edu.ntnu.idi.idatt2003.g40.mappe.service.FileConverter; -import edu.ntnu.idi.idatt2003.g40.mappe.service.FileParser; import edu.ntnu.idi.idatt2003.g40.mappe.service.GameStateLoader; import edu.ntnu.idi.idatt2003.g40.mappe.service.SaveGameService; +import edu.ntnu.idi.idatt2003.g40.mappe.service.StockFileManager; +import edu.ntnu.idi.idatt2003.g40.mappe.service.StockFileParser; import edu.ntnu.idi.idatt2003.g40.mappe.service.event.EventManager; import edu.ntnu.idi.idatt2003.g40.mappe.utils.ConfigValues; import edu.ntnu.idi.idatt2003.g40.mappe.utils.ThemeManager; @@ -88,10 +88,10 @@ public void start(final Stage stage) throws Exception { ViewManager viewManager = new ViewManager(stage, eventManager); List stocksInFile; - FileParser parser1 = new FileParser("/sp500.csv"); + StockFileManager fileManager = new StockFileManager("src/main/resources/sp500.csv"); - FileConverter converter1 = new FileConverter(); - stocksInFile = converter1.getStocksFromStrings(parser1.readFile()); + StockFileParser fileParser = new StockFileParser(); + stocksInFile = fileParser.getStocksFromStrings(fileManager.readFile()); Exchange exchange = new Exchange("Exchange", stocksInFile); Player player = new Player("Player 1", new BigDecimal("10000")); diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/SaveGame.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/SaveGame.java index 9dc97ec..ac406fb 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/SaveGame.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/SaveGame.java @@ -1,5 +1,7 @@ package edu.ntnu.idi.idatt2003.g40.mappe.model; +import edu.ntnu.idi.idatt2003.g40.mappe.utils.Validator; + import java.util.Collections; import java.util.List; @@ -72,6 +74,11 @@ public SaveGame(final String name, final int week, final List ownedShares, final List transactions) { + if (!Validator.NOT_EMPTY.isValid(name) + || balance <= 0 + || startingCapital <= 0) { + throw new IllegalArgumentException("Invalid Save configuration!"); + } this.name = name; this.balance = balance; this.startingCapital = startingCapital; diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/event/EventType.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/event/EventType.java index 47aba89..825e9bc 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/event/EventType.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/service/event/EventType.java @@ -91,12 +91,4 @@ public enum EventType implements EventChannel { * but most listeners don't inspect it.

* */ STATE_RESET; - - /** - * {@inheritDoc} - * */ - @Override - public String getName() { - return this.name(); - } } diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/dashboard/DashBoardController.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/dashboard/DashBoardController.java index 83648ed..3165949 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/dashboard/DashBoardController.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/dashboard/DashBoardController.java @@ -234,7 +234,7 @@ public void handleEvent(final EventData data) { if (!stockList.isEmpty()) { Stock first = stockList.getFirst(); BigDecimal owned = player.getPortfolio() - .getTotalSharesBySymbol(first.getSymbol()); + .getTotalShareQuantityBySymbol(first.getSymbol()); getViewElement().setCurrentStock(first, owned.floatValue()); } getViewElement().updateGraph(selectedTimeRange); diff --git a/src/main/resources/saves/Tommy.json b/src/main/resources/saves/Tommy.json new file mode 100644 index 0000000..90979ce --- /dev/null +++ b/src/main/resources/saves/Tommy.json @@ -0,0 +1,43 @@ +{ + "name": "Tommy", + "balance": 93987.07495, + "startingCapital": 100000.0, + "stockDataPath": "C:\\Users\\tohja\\Desktop\\StockData1.txt", + "week": 18, + "ownedShares": [ + { "symbol": "NVDA", "quantity": 31.0, "purchasePrice": 191.27 } + ], + "transactions": [ + { "type": "PURCHASE", "symbol": "NVDA", "quantity": 1.0, "price": 191.27, "week": 1 }, + { "type": "PURCHASE", "symbol": "NVDA", "quantity": 1.0, "price": 191.27, "week": 1 }, + { "type": "PURCHASE", "symbol": "NVDA", "quantity": 1.0, "price": 191.27, "week": 1 }, + { "type": "PURCHASE", "symbol": "NVDA", "quantity": 1.0, "price": 191.27, "week": 1 }, + { "type": "PURCHASE", "symbol": "NVDA", "quantity": 1.0, "price": 191.27, "week": 1 }, + { "type": "PURCHASE", "symbol": "NVDA", "quantity": 1.0, "price": 191.27, "week": 1 }, + { "type": "PURCHASE", "symbol": "NVDA", "quantity": 1.0, "price": 191.27, "week": 1 }, + { "type": "PURCHASE", "symbol": "NVDA", "quantity": 1.0, "price": 191.27, "week": 1 }, + { "type": "PURCHASE", "symbol": "NVDA", "quantity": 1.0, "price": 191.27, "week": 1 }, + { "type": "PURCHASE", "symbol": "NVDA", "quantity": 1.0, "price": 191.27, "week": 1 }, + { "type": "PURCHASE", "symbol": "NVDA", "quantity": 1.0, "price": 191.27, "week": 1 }, + { "type": "PURCHASE", "symbol": "NVDA", "quantity": 1.0, "price": 191.27, "week": 1 }, + { "type": "PURCHASE", "symbol": "NVDA", "quantity": 1.0, "price": 191.27, "week": 1 }, + { "type": "PURCHASE", "symbol": "NVDA", "quantity": 1.0, "price": 191.27, "week": 1 }, + { "type": "PURCHASE", "symbol": "NVDA", "quantity": 1.0, "price": 191.27, "week": 1 }, + { "type": "PURCHASE", "symbol": "NVDA", "quantity": 1.0, "price": 191.27, "week": 1 }, + { "type": "PURCHASE", "symbol": "NVDA", "quantity": 1.0, "price": 191.27, "week": 1 }, + { "type": "PURCHASE", "symbol": "NVDA", "quantity": 1.0, "price": 191.27, "week": 1 }, + { "type": "PURCHASE", "symbol": "NVDA", "quantity": 1.0, "price": 191.27, "week": 1 }, + { "type": "PURCHASE", "symbol": "NVDA", "quantity": 1.0, "price": 195.74, "week": 4 }, + { "type": "PURCHASE", "symbol": "NVDA", "quantity": 1.0, "price": 195.74, "week": 4 }, + { "type": "PURCHASE", "symbol": "NVDA", "quantity": 1.0, "price": 195.74, "week": 4 }, + { "type": "PURCHASE", "symbol": "NVDA", "quantity": 1.0, "price": 195.74, "week": 4 }, + { "type": "PURCHASE", "symbol": "NVDA", "quantity": 1.0, "price": 195.74, "week": 4 }, + { "type": "PURCHASE", "symbol": "NVDA", "quantity": 1.0, "price": 195.74, "week": 4 }, + { "type": "PURCHASE", "symbol": "NVDA", "quantity": 1.0, "price": 195.74, "week": 4 }, + { "type": "PURCHASE", "symbol": "NVDA", "quantity": 1.0, "price": 195.74, "week": 4 }, + { "type": "PURCHASE", "symbol": "NVDA", "quantity": 1.0, "price": 195.74, "week": 4 }, + { "type": "PURCHASE", "symbol": "NVDA", "quantity": 1.0, "price": 195.74, "week": 4 }, + { "type": "PURCHASE", "symbol": "NVDA", "quantity": 1.0, "price": 195.74, "week": 4 }, + { "type": "PURCHASE", "symbol": "NVDA", "quantity": 1.0, "price": 195.74, "week": 4 } + ] +}