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 }
+ ]
+}