diff --git a/.gitignore b/.gitignore index 70cdfab..5b1c7f5 100644 --- a/.gitignore +++ b/.gitignore @@ -55,5 +55,7 @@ failsafe-reports/ coverage/ jacoco.exec -# ====== Stock data files ======= # -.txt \ No newline at end of file +# ====== Stock data files and save files ======= # +*.txt +*.json +*.csv \ No newline at end of file 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 52f90fd..348cf5c 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 @@ -8,7 +8,10 @@ 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.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.ConfigValues; import edu.ntnu.idi.idatt2003.g40.mappe.utils.ThemeManager; import edu.ntnu.idi.idatt2003.g40.mappe.view.ViewManager; @@ -24,18 +27,17 @@ import edu.ntnu.idi.idatt2003.g40.mappe.view.playgame.PlayGameView; import edu.ntnu.idi.idatt2003.g40.mappe.view.settings.SettingsController; import edu.ntnu.idi.idatt2003.g40.mappe.view.settings.SettingsView; -import java.math.BigDecimal; -import java.util.List; -import java.util.Objects; - import edu.ntnu.idi.idatt2003.g40.mappe.view.widgets.WidgetEnum; -import edu.ntnu.idi.idatt2003.g40.mappe.view.widgets.financialsummary.SummaryController; -import edu.ntnu.idi.idatt2003.g40.mappe.view.widgets.financialsummary.SummaryView; import edu.ntnu.idi.idatt2003.g40.mappe.view.widgets.dashboard.DashBoardController; import edu.ntnu.idi.idatt2003.g40.mappe.view.widgets.dashboard.DashBoardView; +import edu.ntnu.idi.idatt2003.g40.mappe.view.widgets.financialsummary.SummaryController; +import edu.ntnu.idi.idatt2003.g40.mappe.view.widgets.financialsummary.SummaryView; import edu.ntnu.idi.idatt2003.g40.mappe.view.widgets.market.MarketController; import edu.ntnu.idi.idatt2003.g40.mappe.view.widgets.market.MarketView; -import edu.ntnu.idi.idatt2003.g40.mappe.view.widgets.minigames.*; +import edu.ntnu.idi.idatt2003.g40.mappe.view.widgets.minigames.GameEngineController; +import edu.ntnu.idi.idatt2003.g40.mappe.view.widgets.minigames.GameEngineView; +import edu.ntnu.idi.idatt2003.g40.mappe.view.widgets.minigames.MiniGamesController; +import edu.ntnu.idi.idatt2003.g40.mappe.view.widgets.minigames.MiniGamesView; import edu.ntnu.idi.idatt2003.g40.mappe.view.widgets.minigames.games.ClickerGame; import edu.ntnu.idi.idatt2003.g40.mappe.view.widgets.minigames.games.FindStockGame; import edu.ntnu.idi.idatt2003.g40.mappe.view.widgets.minigames.games.TimeInputsGame; @@ -45,6 +47,9 @@ import edu.ntnu.idi.idatt2003.g40.mappe.view.widgets.topbar.TopBarView; import edu.ntnu.idi.idatt2003.g40.mappe.view.widgets.transactions.TransactionsController; import edu.ntnu.idi.idatt2003.g40.mappe.view.widgets.transactions.TransactionsView; +import java.math.BigDecimal; +import java.util.List; +import java.util.Objects; import javafx.application.Application; import javafx.scene.Scene; import javafx.scene.layout.Pane; @@ -54,23 +59,31 @@ /** * Main class. * - *

- * Extends {@link Application} - *

+ *

Extends {@link Application}

* - *

- * Initializes the application through the javafx framework. - *

- */ + *

Launches the javafx thread and starts the program.

+ * */ public class Main extends Application { - static void main(String[] args) { + /** + * Active {@link Exchange} object. + * */ + private Exchange exchange; + + /** + * Active {@link Player} object. + * */ + private Player player; + + /** + * Main method, launches the start javafx method. + * + * @param args standard Java parameter. + * */ + public static void main(final String[] args) { launch(args); } - /** - * {@inheritDoc} - */ @Override public void start(final Stage stage) throws Exception { Scene scene = new Scene(new Pane()); @@ -81,145 +94,134 @@ public void start(final Stage stage) throws Exception { stage.setWidth(ConfigValues.VIEWPORT_WIDTH.getValue()); stage.setHeight(ConfigValues.VIEWPORT_HEIGHT.getValue()); - // Register the scene with the theme manager so it can toggle dark/light. ThemeManager.getInstance().registerScene(scene); EventManager eventManager = new EventManager(); ViewManager viewManager = new ViewManager(stage, eventManager); - List stocksInFile; + // Loads a default file in case of misreading (fallback) StockFileManager fileManager = new StockFileManager("src/main/resources/sp500.csv"); - StockFileParser fileParser = new StockFileParser(); - stocksInFile = fileParser.getStocksFromStrings(fileManager.readFile()); + List stocksInFile = fileParser.getStocksFromStrings(fileManager.readFile()); - Exchange exchange = new Exchange("Exchange", stocksInFile); - Player player = new Player("Player 1", new BigDecimal("10000")); + exchange = new Exchange("Exchange", stocksInFile); + 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); - // Play game (mellom hovedmeny og spillet) PlayGameView playGameView = new PlayGameView(); SaveGameService saveGameService = new SaveGameService(); PlayGameController playGameController = new PlayGameController(playGameView, eventManager, saveGameService); playGameController.refresh(); - - // Create game (mellom play-game og selve spillet) CreateGameView createGameView = new CreateGameView(); CreateGameController createGameController = new CreateGameController(createGameView, eventManager, saveGameService); - // Refresh save-listen etter at en ny save er skrevet til disk. createGameController.setOnSaveCreated(playGameController::refresh); - // Settings SettingsView settingsView = new SettingsView(); new SettingsController(settingsView, eventManager); - // Summary section of the top bar SummaryView summaryView = new SummaryView(); - new SummaryController(summaryView, eventManager, exchange, player); + SummaryController summaryController = + new SummaryController(summaryView, eventManager, exchange, player); - // Top bar with summary section TopBarView topBarView = new TopBarView(summaryView); TopBarController topBarController = new TopBarController(topBarView, eventManager); - // Top bar without summary section TopBarView topBarView2 = new TopBarView(); new TopBarController(topBarView2, eventManager); - // Dashboard (default center-view in-game - første siden du ser) DashBoardView dashBoardView = new DashBoardView(); - new DashBoardController(dashBoardView, - eventManager, - player, - exchange, - stocksInFile); + DashBoardController dashBoardController = + new DashBoardController(dashBoardView, eventManager, player, exchange, stocksInFile); - // Stats page (Stats-knappen i topbaren tar deg hit) StatsView statsView = new StatsView(); - new StatsController(statsView, eventManager, player, exchange); + StatsController statsController = + new StatsController(statsView, eventManager, player, exchange); - // Market page (Market-knappen tar deg hit) MarketView marketView = new MarketView(); - new MarketController(marketView, - eventManager, - player, - exchange, - stocksInFile); - - // In-game (Change "topBarView" to "topBarView2" if no summary section). - // Dashboard er default center-view. + MarketController marketController = + new MarketController(marketView, eventManager, player, exchange, stocksInFile); + InGameView inGameView = new InGameView(topBarView, dashBoardView.getRootPane()); - InGameController inGameController = new InGameController( - inGameView, - eventManager - ); + InGameController inGameController = new InGameController(inGameView, eventManager); - // Transaction history page TransactionsView transactionsView = new TransactionsView(); TransactionsController transactionsController = new TransactionsController( - transactionsView, - eventManager, - player.getTransactionArchive()); + transactionsView, eventManager, player.getTransactionArchive()); ClickerGame clickerGame = new ClickerGame(); FindStockGame findStockGame = new FindStockGame( - stocksInFile.stream() - .map(Stock::getSymbol) - .toList() + stocksInFile.stream().map(Stock::getSymbol).toList() ); TimeInputsGame timeInputsGame = new TimeInputsGame(); GameEngineView gameEngineView = new GameEngineView(); GameEngineController gameEngineController = new GameEngineController( - gameEngineView, - eventManager, - stocksInFile.getFirst() + gameEngineView, eventManager, stocksInFile.getFirst() ); MiniGamesView miniGamesView = new MiniGamesView(); - new MiniGamesController( - miniGamesView, - eventManager, - stocksInFile.getFirst(), - gameEngineView, - gameEngineController, - clickerGame, - inGameView, - findStockGame, - timeInputsGame + MiniGamesController miniGamesController = + new MiniGamesController( + miniGamesView, eventManager, stocksInFile.getFirst(), gameEngineView, + gameEngineController, clickerGame, inGameView, findStockGame, timeInputsGame ); - // Wire top bar buttons til å bytte mellom dashboard / stats / market / - // transactions. Stats-knappen tar deg til stats-siden. + // Adds a generic event subscriber to the event manager + // that handles STATE_RESET events. These events are + // triggered when a new save file is loaded. + // We define this implementation here, because + // this class has the highest level overview of the application. + eventManager.addSubscriber(new EventSubscriber() { + @Override + public void handleEvent(final EventData eventData) { + if (eventData.data() instanceof SaveGame) { + + if (createGameController.getExchange() != null && createGameController.getPlayer() != null) { + gameStateLoader.setExchange(createGameController.getExchange()); + + exchange = createGameController.getExchange(); + player = createGameController.getPlayer(); + } else { + exchange = gameStateLoader.getExchange(); + player = gameStateLoader.getPlayer(); + } + + List dynamicStocks = exchange.getStocks(); + + // Updates the various UI components that + // depend on an exchange, player, or list of stocks. + findStockGame.updateStockPool(dynamicStocks.stream().map(Stock::getSymbol).toList()); + summaryController.handleContextUpdate(Main.this.exchange, Main.this.player); + dashBoardController.handleStockPoolUpdate(dynamicStocks); + statsController.handleContextUpdate(Main.this.exchange, Main.this.player); + marketController.handleStockPoolUpdate(Main.this.exchange, Main.this.player, dynamicStocks); + + if (!dynamicStocks.isEmpty()) { + miniGamesController.setActiveStock(dynamicStocks.getFirst()); + } + } + } + }, EventType.STATE_RESET); + topBarController.setMarketIntegration( - inGameView::changeCenterView, - dashBoardView.getRootPane(), - marketView.getRootPane(), - statsView.getRootPane(), - transactionsView.getRootPane(), - transactionsController::refresh, + inGameView::changeCenterView, dashBoardView.getRootPane(), marketView.getRootPane(), + statsView.getRootPane(), transactionsView.getRootPane(), transactionsController::refresh, miniGamesView.getRootPane() ); - // In-game settings overlay (Dark / Light theme toggle). InGameSettingsView inGameSettingsView = new InGameSettingsView(); InGameSettingsController inGameSettingsController = 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(); @@ -227,21 +229,14 @@ public void start(final Stage stage) throws Exception { 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); viewManager.addView(createGameView); @@ -249,7 +244,6 @@ public void start(final Stage stage) throws Exception { viewManager.addView(inGameView); viewManager.setScene(mainMenuView); - // Register all widgets inGameController.addwidget(WidgetEnum.DASHBOARD, dashBoardView.getRootPane()); inGameController.addwidget(WidgetEnum.MARKET, marketView.getRootPane()); inGameController.addwidget(WidgetEnum.MINIGAMES_OVERVIEW, miniGamesView.getRootPane()); @@ -259,4 +253,4 @@ public void start(final Stage stage) throws Exception { stage.show(); } -} \ No newline at end of file +} 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 0ef3b95..bfeaad4 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 @@ -24,7 +24,8 @@ * *

Delegates buying and selling to player elements using calculators

* - *

Advances week.

+ *

Additionally controls the time in weeks, and therefore has the main + * responsibility for advancing weeks.

* * @see Player * @see edu.ntnu.idi.idatt2003.g40.mappe.service.TransactionCalculator @@ -109,7 +110,7 @@ public ReadOnlyIntegerProperty weekProperty() { *

* 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 + * exchange. The {@link ReadOnlyIntegerProperty} fires a change event so any * listeners that re-render on week changes refresh themselves. *

* @@ -163,6 +164,15 @@ public boolean hasStock(final String symbol) { return stockMap.containsKey(symbol); } + /** + * Getter method for all stocks in exchange. + * + * @return List of stocks. + * */ + public List getStocks() { + return List.copyOf(this.stockMap.values()); + } + /** * Getter method for stock element. * @@ -363,7 +373,7 @@ public List getGainers(final int limit) * Method for getting the stocks with the highest * loss of price since last week. * - * @param limit the maximum amount of stocks returned + * @param limit the maximum amount of stocks returned * * @return list of {@link Stock} objects. * @@ -387,4 +397,25 @@ public List getLosers(final int limit) .map(Map.Entry::getValue) .toList(); } + + /** + * Replaces list of stocks with a new list. + * + *

Used when loading a save game that uses a completely different set of + * stocks than the currently active simulation context.

+ * + * @param newStocks the new list of {@link Stock} + * objects to list on the exchange. + * @throws IllegalArgumentException if the provided list is null or empty. + */ + public void updateStockPool(final List newStocks) throws IllegalArgumentException { + if (newStocks == null || newStocks.isEmpty()) { + throw new IllegalArgumentException("New stock pool cannot be null or empty!"); + } + + this.stockMap.clear(); + for (Stock stock : newStocks) { + this.stockMap.put(stock.getSymbol(), stock); + } + } } 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 9e5ac5f..cf63d80 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 @@ -5,6 +5,8 @@ import edu.ntnu.idi.idatt2003.g40.mappe.exceptions.NotEnoughMoneyException; import edu.ntnu.idi.idatt2003.g40.mappe.utils.Validator; import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; import javafx.beans.property.ReadOnlyFloatProperty; import javafx.beans.property.ReadOnlyFloatWrapper; @@ -39,6 +41,11 @@ public final class Player { * */ private BigDecimal money; + /** + * History record tracking the player's net worth values over time. + */ + private final List netWorthHistory = new ArrayList<>(); + /** * Current net-worth of player as a listenable, * read-only, {@link ReadOnlyFloatWrapper} object. @@ -91,6 +98,7 @@ public Player(final String name, this.money = this.startingMoney; this.portfolio = new Portfolio(); this.transactionArchive = new TransactionArchive(); + this.netWorthHistory.add(this.startingMoney); updateObservableProperties(); } @@ -121,6 +129,27 @@ public BigDecimal getMoney() { return money; } + /** + * Returns a read-only list of the player's net worth history. + * + * @return read only list of players net-worth instances. + */ + public List getNetWorthHistory() { + return List.copyOf(netWorthHistory); + } + + /** + * Replaces the net-worth history list of this player with a new list. + * + * @param history the list to replace current list with. + */ + public void setNetWorthHistory(final List history) { + this.netWorthHistory.clear(); + if (history != null) { + this.netWorthHistory.addAll(history); + } + } + /** * Adds money to the players balance. * @@ -185,7 +214,7 @@ public void setMoney(final BigDecimal newMoney) throws IllegalArgumentException /** * Re-publishes the players current money and net-worth to the - * {@link FloatProperty} listeners. + * {@link ReadOnlyFloatWrapper} listeners. * *

* Used after the save-loading flow has mutated the player state 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 ac406fb..27a938d 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,31 +1,17 @@ package edu.ntnu.idi.idatt2003.g40.mappe.model; import edu.ntnu.idi.idatt2003.g40.mappe.utils.Validator; - +import java.math.BigDecimal; import java.util.Collections; import java.util.List; /** - * Represents one save game entry. + * Represents one save entry for the game. * - *

- * Holds the display name, current balance, starting capital, + *

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. - *

+ * the list of owned shares, the list of committed transactions, and + * the current state of the exchange stocks for a single saved game.

*/ public class SaveGame { @@ -41,7 +27,7 @@ public class SaveGame { /** * Absolute path to a custom stock data file, or {@code null} * if the save should use the default bundled stock data. - * */ + */ private final String stockDataPath; /** In-game week when the save was last written. */ @@ -53,19 +39,25 @@ public class SaveGame { /** Committed transactions when the save was last written. */ private final List transactions; + /** The snapshot state of the exchange's stocks when saved. */ + private final List exchangeStocks; + + /** The snapshot state of the players's networth history when saved. */ + private final List netWorthHistory; + /** - * Full constructor. + * Constructor. * * @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. + * 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). + * @param ownedShares the shares the player owned (may be null; treated as empty). + * @param transactions the committed transactions (may be null; treated as empty). + * @param exchangeStocks the current list of stocks from the exchange (may be null; treated as empty). + * @param netWorthHistory the history profile tracking trajectory (may be null; treated as empty). */ public SaveGame(final String name, final double balance, @@ -73,7 +65,9 @@ public SaveGame(final String name, final String stockDataPath, final int week, final List ownedShares, - final List transactions) { + final List transactions, + final List exchangeStocks, + final List netWorthHistory) { if (!Validator.NOT_EMPTY.isValid(name) || balance <= 0 || startingCapital <= 0) { @@ -90,49 +84,105 @@ public SaveGame(final String name, this.transactions = (transactions != null) ? List.copyOf(transactions) : Collections.emptyList(); + this.exchangeStocks = (exchangeStocks != null) + ? List.copyOf(exchangeStocks) + : Collections.emptyList(); + this.netWorthHistory = (netWorthHistory != null) + ? List.copyOf(netWorthHistory) + : Collections.emptyList(); } /** - * Convenience constructor matching the original "name + balance + capital - * + stockDataPath" format used by the create-game flow. + * Overloaded constructor that converts a List of Floats to BigDecimals. * - *

- * 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. - *

+ *

Used by JSON parser to map array

* * @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. - * */ + * 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). + * @param exchangeStocks the current list of stocks from the exchange (may be null; treated as empty). + * @param dummyFlagForOverloading sample dummy flag used for overload. + */ public SaveGame(final String name, final double balance, final double startingCapital, - final String stockDataPath) { - this(name, balance, startingCapital, stockDataPath, - 1, Collections.emptyList(), Collections.emptyList()); + final String stockDataPath, + final int week, + final List ownedShares, + final List transactions, + final List exchangeStocks, + final List floatNetWorthHistory, + final boolean dummyFlagForOverloading) { + this(name, balance, startingCapital, stockDataPath, week, ownedShares, transactions, exchangeStocks, + (floatNetWorthHistory != null) + ? floatNetWorthHistory.stream().map(BigDecimal::valueOf).toList() + : Collections.emptyList()); } /** - * Convenience constructor matching the legacy "name + balance" format. + * Backwards-compatible overload used in a previous implementation. * - *

- * Starting capital is defaulted to the balance value, - * {@code stockDataPath} is left {@code null} so the default stock - * 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. + * @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). + * @param exchangeStocks the current list of stocks from the exchange (may be null; treated as empty). + */ + public SaveGame(final String name, + final double balance, + final double startingCapital, + final String stockDataPath, + final int week, + final List ownedShares, + final List transactions, + final List exchangeStocks) { + this(name, balance, startingCapital, stockDataPath, week, ownedShares, transactions, exchangeStocks, Collections.emptyList()); + } + + /** + * Backwards-compatible overload used in a previous implementation. * - * @param name the display name of the save. - * @param balance the current balance value. + * @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. + * @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) { - this(name, balance, balance, null, - 1, Collections.emptyList(), Collections.emptyList()); + public SaveGame(final String name, + final double balance, + final double startingCapital, + final String stockDataPath, + final int week, + final List ownedShares, + final List transactions) { + this(name, balance, startingCapital, stockDataPath, week, ownedShares, transactions, Collections.emptyList(), Collections.emptyList()); + } + + /** + * Convenience constructor matching the original format used by the create-game flow. + * @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(), Collections.emptyList(), Collections.emptyList()); } /** @@ -153,6 +203,15 @@ public double getBalance() { return balance; } + /** + * Getter method net-worth history. + * + * @return an immutable snapshot view of the net worth history. + */ + public List getNetWorthHistory() { + return netWorthHistory; + } + /** * Getter method for the starting capital. * @@ -166,7 +225,7 @@ public double getStartingCapital() { * Getter method for the custom stock data path. * * @return the absolute path to a custom stock data file, - * or {@code null} if the save uses the default data. + * or {@code null} if the save uses the default data. */ public String getStockDataPath() { return stockDataPath; @@ -176,7 +235,7 @@ public String getStockDataPath() { * Getter method for the in-game week. * * @return the week the save was last written at. - * */ + */ public int getWeek() { return week; } @@ -185,7 +244,7 @@ public int getWeek() { * Getter method for the owned shares. * * @return an immutable list of {@link OwnedShareData} entries. - * */ + */ public List getOwnedShares() { return ownedShares; } @@ -194,8 +253,17 @@ public List getOwnedShares() { * Getter method for the committed transactions. * * @return an immutable list of {@link TransactionData} entries. - * */ + */ public List getTransactions() { return transactions; } + + /** + * Getter method for the captured state of exchange stocks. + * + * @return an immutable list of {@link Stock} objects saved inside this snapshot. + */ + public List getExchangeStocks() { + return exchangeStocks; + } } 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 index 8cbd29e..874f6ec 100644 --- 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 @@ -17,7 +17,6 @@ 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; @@ -25,86 +24,50 @@ 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.).
  • - *
+ * Service that applies a {@link SaveGame} to the active {@link Player} and {@link Exchange}. * - *

- * 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. - *

+ *

Calls events of type

*/ -public final class GameStateLoader - implements EventSubscriber, EventPublisher { +public final class GameStateLoader implements EventSubscriber, EventPublisher { - /** Active {@link Player} instance being mutated on load. */ + /** + * Local reference to player. + * */ private final Player player; - /** Active {@link Exchange} instance being mutated on load. */ - private final Exchange exchange; + /** + * Local reference to exchange. + * */ + private Exchange exchange; - /** Used to publish {@link EventType#STATE_RESET} after a load. */ + /** + * Local reference to event manager. + * */ 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. - *

+ * Baseline prices of symbols. * */ 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.

+ * Currently active save. * */ 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. + * @param player player object. + * @param exchange exchange. + * @param stockList list of stocks for the game state. + * @param eventManager the event manager instance. * */ 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) { + final EventManager eventManager) throws IllegalArgumentException { + if (player == null || exchange == null || stockList == null || eventManager == null) { throw new IllegalArgumentException("Invalid GameStateLoader arguments!"); } this.player = player; @@ -115,31 +78,41 @@ public GameStateLoader(final Player player, } /** - * Getter method for the {@link SaveGame} most recently loaded. + * Setter method for exchange. * - * @return the active save, or {@code null} if none has been loaded. - * */ - public SaveGame getActiveSave() { - return activeSave; + * @param dynamicExchange new exchange. + */ + public void setExchange(final Exchange dynamicExchange) { + if (dynamicExchange != null) { + this.exchange = dynamicExchange; + } } /** - * Builds a fresh {@link SaveGame} snapshot of the current player - * and exchange state, using the active save's name and starting - * capital as identity. + * Getter method for exchange. * - *

- * 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 exchange. + * */ + public Exchange getExchange() { + return exchange; + } + + /** + * Getter method for player. * - * @return a snapshot {@link SaveGame}, or {@code null}. + * @return player. * */ + public Player getPlayer() { + return player; + } + + /** + * Builds a new {@link SaveGame} snapshot, + * passing along the current exchange state. + * + * @return {@link SaveGame} object. + */ public SaveGame snapshotActiveSave() { - if (activeSave == null) { - return null; - } List shares = new ArrayList<>(); for (Share s : player.getPortfolio().getShares()) { shares.add(new OwnedShareData( @@ -150,9 +123,7 @@ public SaveGame snapshotActiveSave() { List txns = new ArrayList<>(); for (Transaction t : player.getTransactionArchive().getTransactions()) { - TransactionType type = (t instanceof Sale) - ? TransactionType.SALE - : TransactionType.PURCHASE; + TransactionType type = (t instanceof Sale) ? TransactionType.SALE : TransactionType.PURCHASE; txns.add(new TransactionData( type, t.getShare().getStock().getSymbol(), @@ -161,68 +132,61 @@ public SaveGame snapshotActiveSave() { t.getWeek())); } + String saveName = (activeSave != null) ? activeSave.getName() : player.getName(); + double startingCap = (activeSave != null) ? activeSave.getStartingCapital() : player.getStartingMoney().doubleValue(); + String stockPath = (activeSave != null) ? activeSave.getStockDataPath() : null; + return new SaveGame( - activeSave.getName(), + saveName, player.getMoney().doubleValue(), - activeSave.getStartingCapital(), - activeSave.getStockDataPath(), + startingCap, + stockPath, exchange.getWeek(), shares, - txns); + txns, + exchange.getStocks(), + player.getNetWorthHistory()); } - /** {@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!"); + 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) { + 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. + * Applies a save. * - *

- * 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. - *

+ *

If save includes valid stocks, uses them. If not, + * uses fallback stocks (default data).

* - * @param save the {@link SaveGame} to apply. + *

Clears and rebuilds exchange, player, transactions, + * portfolio and net-worth history of player.

+ * + * @param save save 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()); + Map activeBaseline; + if (save.getExchangeStocks() != null && !save.getExchangeStocks().isEmpty()) { + activeBaseline = captureBaseline(save.getExchangeStocks()); + + this.exchange.updateStockPool(save.getExchangeStocks()); + } else { + activeBaseline = this.baselinePrices; + } - // 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.resetStocksTo(activeBaseline); 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(); @@ -230,39 +194,23 @@ private void applySave(final SaveGame save) { 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()); + 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())); + 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()); + System.err.println("Skipping transaction with unknown stock: " + td.getSymbol()); continue; } Stock stock = exchange.getStock(td.getSymbol()); @@ -276,21 +224,25 @@ private void applySave(final SaveGame save) { 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. + if (save.getNetWorthHistory() != null) { + player.setNetWorthHistory(save.getNetWorthHistory()); + } else { + player.setNetWorthHistory(new ArrayList<>()); + } + 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. + * Creates a new map keyed by stock symbol and valued by latest stock price. + * + * @param stocks stocks to use when creating map. + * + * @return map keyed with stock symbol and valued with stock price. * */ private static Map captureBaseline(final List stocks) { Map snapshot = new HashMap<>(); 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 aed5026..cf86ca5 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 @@ -2,8 +2,8 @@ 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.Stock; import edu.ntnu.idi.idatt2003.g40.mappe.model.TransactionData; - import java.io.IOException; import java.math.BigDecimal; import java.nio.charset.StandardCharsets; @@ -21,10 +21,8 @@ /** * Service for loading and saving {@link SaveGame} entries from disk. * - *

- * Each save is stored as a separate JSON file in a single directory, - * which lets several saves co-exist independently. - *

+ *

Each save is stored as a separate JSON file in a single directory, + * which lets several saves co-exist independently.

* *

JSON file format:

* @@ -42,23 +40,26 @@ * { "type": "PURCHASE", "symbol": "AAPL", "quantity": 5, * "price": 150.00, "week": 1 } * ] + * "stocks": [ + * { "symbol": "AAPL", "companyName": "Apple Inc.", "salesPrice": 155.25 } + * ] + * "netWorthHistory": [ + * 10000.00, + * 10026.25 + * ] * } * * - *

- * The {@code week}, {@code ownedShares} and {@code transactions} + *

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

+ * defaults (week 1, empty lists).

* - *

- * The service exposes both a "load all saves" entry point (used by + *

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 and the * auto-save flow). The JSON parsing logic is intentionally minimal and * tailored to this format - invalid files are skipped rather than - * throwing. - *

+ * throwing.

*/ public class SaveGameService { @@ -90,16 +91,12 @@ public SaveGameService(final String directoryPath) { /** * Loads all save games from the directory. * - *

- * Walks the saves directory, parses every file with a + *

Walks the saves directory, parses every file with a * {@code .json} extension and returns the resulting list. - * Files that fail to parse are silently skipped. - *

+ * Files that fail to parse are silently skipped.

* - *

- * Returns an empty list if the directory does not exist or cannot - * be read. - *

+ *

Returns an empty list if the directory does not exist or cannot + * be read.

* * @return the loaded {@link SaveGame} entries. */ @@ -133,12 +130,10 @@ public List loadSaves() { /** * Writes a single {@link SaveGame} to disk as a JSON file. * - *

- * The file name is derived from {@link SaveGame#getName()} sanitised + *

The file name is derived from {@link SaveGame#getName()} sanitised * to filesystem-safe characters and given the {@code .json} * extension. The saves directory is created automatically if it - * doesn't exist. - *

+ * doesn't exist.

* * @param save the {@link SaveGame} object to write. * @@ -184,11 +179,9 @@ public boolean saveExists(final String name) { /** * Loads a single save from an explicit file path. * - *

- * Used by the upload flow on the play-game screen so a save file + *

Used by the upload flow on the play-game screen so a save file * located anywhere on disk can be added to the displayed list - * without having to live inside the saves directory. - *

+ * without having to live inside the saves directory.

* * @param file the file to parse. * @return the parsed {@link SaveGame}, or {@code null} if the file @@ -204,12 +197,10 @@ public SaveGame loadSaveFromFile(final Path file) { /** * Parses a single JSON file into a {@link SaveGame}. * - *

- * Returns {@code null} for files that don't contain a valid save + *

Returns {@code null} for files that don't contain a valid save * 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). - *

+ * cleanly, with those fields defaulted (week 1, empty lists).

* * @param file path to the file to parse. * @return the parsed {@link SaveGame}, or {@code null}. @@ -233,13 +224,11 @@ private SaveGame parseFile(final Path file) { ? Double.parseDouble(fields.get("startingCapital")) : balance; - // stockDataPath may be absent or explicitly "null". String stockDataPath = fields.get("stockDataPath"); if (stockDataPath != null && stockDataPath.isEmpty()) { stockDataPath = null; } - // 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")); @@ -248,16 +237,34 @@ private SaveGame parseFile(final Path file) { } } - // 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")); + List exchangeStocks = new ArrayList<>(); + String stocksBody = extractArrayBody(content, "stocks"); + if (stocksBody != null && !stocksBody.trim().isEmpty()) { + for (String objectStr : splitArrayObjects(stocksBody)) { + Map stockFields = parseFlatJsonObject(objectStr); + if (stockFields == null) continue; + + String sym = stockFields.get("symbol"); + String nm = stockFields.get("name"); + String prcStr = stockFields.get("price"); + + if (sym != null && nm != null && prcStr != null) { + try { + exchangeStocks.add(new Stock(sym, nm, new BigDecimal(prcStr))); + } catch (Exception e) { + // Skip individual invalid elements. + } + } + } + } + return new SaveGame(name, balance, startingCapital, stockDataPath, - week, ownedShares, transactions); + week, ownedShares, transactions, exchangeStocks); } catch (IOException | NumberFormatException e) { System.err.println("Skipping invalid save file " + file.getFileName() + ": " + e.getMessage()); @@ -268,13 +275,11 @@ private SaveGame parseFile(final Path file) { /** * Serialises a {@link SaveGame} to a JSON object string. * - *

- * Output is pretty-printed with two-space indentation and a trailing + *

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. Owned shares and transactions - * write as JSON arrays of small flat objects. - *

+ * write as JSON arrays of small flat objects.

* * @param save the save to convert. * @return JSON object string. @@ -283,10 +288,8 @@ private String toJson(final SaveGame save) { StringBuilder sb = new StringBuilder(); sb.append("{\n"); sb.append(" \"name\": ").append(quote(save.getName())).append(",\n"); - sb.append(" \"balance\": ") - .append(formatNumber(save.getBalance())).append(",\n"); - sb.append(" \"startingCapital\": ") - .append(formatNumber(save.getStartingCapital())).append(",\n"); + sb.append(" \"balance\": ").append(formatNumber(save.getBalance())).append(",\n"); + sb.append(" \"startingCapital\": ").append(formatNumber(save.getStartingCapital())).append(",\n"); sb.append(" \"stockDataPath\": "); if (save.getStockDataPath() == null) { sb.append("null"); @@ -295,14 +298,35 @@ private String toJson(final SaveGame save) { } 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(" \"ownedShares\": ").append(ownedSharesToJson(save.getOwnedShares())).append(",\n"); + sb.append(" \"transactions\": ").append(transactionsToJson(save.getTransactions())).append(",\n"); + + sb.append(" \"stocks\": ").append(stocksToJson(save.getExchangeStocks())).append("\n"); sb.append("}\n"); return sb.toString(); } + private String stocksToJson(final List stocks) { + if (stocks == null || stocks.isEmpty()) { + return "[]"; + } + StringBuilder sb = new StringBuilder(); + sb.append("[\n"); + for (int i = 0; i < stocks.size(); i++) { + Stock s = stocks.get(i); + sb.append(" { \"symbol\": ").append(quote(s.getSymbol())) + .append(", \"name\": ").append(quote(s.getCompany())) + .append(", \"price\": ").append(s.getSalesPrice().toPlainString()) + .append(" }"); + if (i < stocks.size() - 1) { + sb.append(","); + } + sb.append("\n"); + } + sb.append(" ]"); + return sb.toString(); + } + /** * Serialises a list of {@link OwnedShareData} entries to a JSON array. * @@ -378,22 +402,16 @@ private String formatNumber(final double value) { * 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": [ ... ]}} + *

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 ]}. - *

+ * between the opening {@code [} and the matching closing {@code ]}.

* - *

- * Returns {@code null} if the field is not present, or if the array + *

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. - *

+ * empty array.

* - *

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

+ *

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. @@ -455,12 +473,10 @@ private String extractArrayBody(final String content, /** * Splits a JSON array body into the substrings of its inner objects. * - *

- * Tracks bracket and quote nesting so that commas inside the inner + *

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. - *

+ * {@code {...}} found.

* * @param body the substring between {@code [} and {@code ]}. * @return list of object substrings, or an empty list. @@ -594,21 +610,17 @@ private List parseTransactionsArray(final String body) { /** * Parses a flat (one level deep) JSON object into a map. * - *

- * Only supports string, number, boolean and null values for the + *

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. - *

+ * can't be parsed.

* - *

- * Values are returned as strings; {@code null} values are stored as + *

Values are returned as strings; {@code null} values are stored as * an empty string so callers can distinguish "absent" from "null" - * using {@link Map#containsKey(Object)}. - *

+ * using {@link Map#containsKey(Object)}.

* * @param content the raw file content. * @return a map of field name to raw value, or {@code null}. @@ -734,11 +746,9 @@ private int findStringEnd(final String s, final int start) { * 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 + *

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}). - *

+ * arrays are read separately by {@link #extractArrayBody}).

* */ private int findStructuredEnd(final String s, final int start) { char open = s.charAt(start); @@ -833,11 +843,9 @@ private String unquote(final String token) { /** * Sanitises a save name to a safe filesystem identifier. * - *

- * All characters outside {@code [A-Za-z0-9_-]} are replaced with + *

All characters outside {@code [A-Za-z0-9_-]} are replaced with * underscores. This keeps the on-disk file name predictable while - * preserving the original display name in the JSON content. - *

+ * preserving the original display name in the JSON content.

*/ private String sanitiseFileName(final String name) { String trimmed = name.trim(); diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/creategame/CreateGameController.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/creategame/CreateGameController.java index e671f6c..ccd0d25 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/creategame/CreateGameController.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/creategame/CreateGameController.java @@ -1,14 +1,18 @@ package edu.ntnu.idi.idatt2003.g40.mappe.view.creategame; +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.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.view.ViewController; import edu.ntnu.idi.idatt2003.g40.mappe.view.ViewEnum; - import java.io.File; import java.io.IOException; - +import java.util.List; import javafx.scene.Scene; import javafx.scene.control.Alert; import javafx.scene.control.Alert.AlertType; @@ -20,22 +24,28 @@ * *

Extends {@link ViewController}.

* - *

- * Handles the four interactions on the create-game screen: picking + *

Handles the four interactions on the create-game screen: picking * the default stock data, choosing a custom stock data file, cancel - * (back to the play-game screen), and finally creating the save. - *

+ * (back to the play-game screen), and finally creating the save.

* - *

- * When a save is successfully written to disk a callback can be - * notified so the play-game view can refresh its save list. - *

+ *

When a save is successfully written to disk a callback can be + * notified so the play-game view can refresh its save list.

*/ public class CreateGameController extends ViewController { /** Service used to write new saves to disk. */ private final SaveGameService saveGameService; + /** + * Exchange defined by a selected list of stocks. + * */ + private Exchange exchange; + + /** + * Player object. + */ + private Player player; + /** * Callback invoked after a save has been written successfully. * Set by {@link #setOnSaveCreated(Runnable)} and defaults to a @@ -62,6 +72,24 @@ public CreateGameController(final CreateGameView view, this.saveGameService = saveGameService; } + /** + * Getter method for exchange. + * + * @return exchange. + * */ + public Exchange getExchange() { + return exchange; + } + + /** + * Getter method for player. + * + * @return player. + * */ + public Player getPlayer() { + return player; + } + /** * Sets the callback invoked after a save has been written to disk. * @@ -78,8 +106,18 @@ public void setOnSaveCreated(final Runnable handler) { */ @Override protected void initInteractions() { - getViewElement().setOnAction(CreateGameActions.USE_DEFAULT_STOCKS, - () -> getViewElement().selectDefaultStocks()); + getViewElement().setOnAction(CreateGameActions.USE_DEFAULT_STOCKS, () -> { + try { + StockFileManager stockFileManager = new StockFileManager("sp500.csv"); + StockFileParser stockFileParser = new StockFileParser(); + List defaultStocks = stockFileParser.getStocksFromStrings(stockFileManager.readFile()); + this.exchange = new Exchange("Exchange", defaultStocks); + + getViewElement().selectDefaultStocks(); + } catch (IOException | IllegalArgumentException e) { + showAlert(AlertType.ERROR, "Feil ved lasting", "Kunne ikke laste standard aksjedata: " + e.getMessage()); + } + }); getViewElement().setOnAction(CreateGameActions.CHOOSE_STOCK_FILE, this::handleChooseStockFile); @@ -98,19 +136,32 @@ protected void initInteractions() { */ private void handleChooseStockFile() { FileChooser fileChooser = new FileChooser(); - fileChooser.setTitle("Velg aksje fil"); + fileChooser.setTitle("Choose Stock File"); fileChooser.getExtensionFilters().addAll( - new FileChooser.ExtensionFilter("Stock data (*.txt)", "*.txt"), + new FileChooser.ExtensionFilter("Stock data (*.csv)", "*.csv"), new FileChooser.ExtensionFilter("All files", "*.*") ); - Window window = getOwnerWindow(); File selectedFile = fileChooser.showOpenDialog(window); if (selectedFile == null) { - // User cancelled the dialog. return; } - + StockFileManager stockFileManager = new StockFileManager(selectedFile.getAbsolutePath()); + StockFileParser stockFileParser = new StockFileParser(); + List fileValues; + try { + fileValues = stockFileManager.readFile(); + } catch (IOException e) { + showAlert(AlertType.ERROR, "Error when reading", "Could not read selected file."); + return; + } + try { + List stockList = stockFileParser.getStocksFromStrings(fileValues); + this.exchange = new Exchange("Exchange", stockList); + } catch (IllegalArgumentException e) { + showAlert(AlertType.ERROR, "Invalid format", "The file contains an invalid format."); + return; + } getViewElement().selectCustomStockFile(selectedFile); } @@ -124,8 +175,8 @@ private void handleCreateGame() { String name = view.getFileName(); Double capital = view.getStartingCapital(); CreateGameView.StockSelection selection = view.getStockSelection(); - - // The button should be disabled in this case, but guard anyway. + this.player = new edu.ntnu.idi.idatt2003.g40.mappe.model.Player(name, + java.math.BigDecimal.valueOf(capital)); if (name.isEmpty() || capital == null || selection == CreateGameView.StockSelection.NONE) { @@ -148,23 +199,33 @@ private void handleCreateGame() { } } + List initialStocks = (this.exchange != null) ? this.exchange.getStocks() : List.of(); + SaveGame newSave = new SaveGame( name, capital, capital, - stockDataPath + stockDataPath, + 1, + List.of(), + List.of(), + initialStocks ); try { saveGameService.saveGame(newSave); } catch (IOException | IllegalArgumentException e) { showAlert(AlertType.ERROR, - "Kunne ikke lagre", - "Klarte ikke å skrive save-filen:\n" + e.getMessage()); + "Could not save.", + "Could not save file:\n" + e.getMessage()); return; } onSaveCreated.run(); + + this.exchange = null; + this.player = null; + changeScene(ViewEnum.PLAY_GAME); } 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 3165949..9a5cd9c 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 @@ -13,6 +13,7 @@ import javafx.scene.control.TextFormatter; import java.math.BigDecimal; import java.math.RoundingMode; +import java.util.ArrayList; import java.util.List; /** @@ -58,7 +59,7 @@ public DashBoardController(final DashBoardView viewElement, final List stockList) throws IllegalArgumentException { this.player = player; - this.stockList = stockList; + this.stockList = new ArrayList<>(stockList); this.exchange = exchange; this.selectedFilter = ""; this.selectedTimeRange = DashBoardTimeRange.DEFAULT; @@ -219,6 +220,28 @@ protected void initInteractions() { }); } + /** + * Method for setting a new list of stocks for the view and controller. + * + * @param updatedStocks new list of stocks. + * */ + public void handleStockPoolUpdate(final List updatedStocks) { + if (updatedStocks == null || updatedStocks.isEmpty()) { + return; + } + + this.stockList.clear(); + this.stockList.addAll(updatedStocks); + + populateStockList(this.selectedFilter); + + Stock firstStock = this.stockList.getFirst(); + BigDecimal ownedAmount = this.player.getPortfolio() + .getTotalShareQuantityBySymbol(firstStock.getSymbol()); + + handleStockSelection(firstStock, ownedAmount.floatValue()); + } + /** * Refresh the dashboard view after a save has been loaded. * @@ -228,8 +251,6 @@ protected void initInteractions() { * */ @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(); 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 6c9940e..ca060f3 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 @@ -7,6 +7,7 @@ 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; @@ -15,7 +16,7 @@ public class SummaryController extends ViewController private Exchange exchange; private Player player; - private List playerNetWorthHistory; + private List playerNetWorthHistory; /** * {@inheritDoc}. @@ -31,21 +32,23 @@ public SummaryController(final SummaryView viewElement, super(viewElement, eventManager); eventManager.addSubscriber(this, EventType.STATE_RESET); eventManager.addSubscriber(this, EventType.PRE_LOAD_SAVE); - getViewElement().setBalance(player.getStartingMoney().floatValue(), player.getStartingMoney().floatValue()); + getViewElement().setBalance(player.getStartingMoney().floatValue(), + player.getStartingMoney().floatValue()); getViewElement().setStatus(player.getStatus()); } @Override protected void initInteractions() { - playerNetWorthHistory.add(player.getNetWorth().floatValue()); + playerNetWorthHistory.add(player.getNetWorth()); + player.setNetWorthHistory(playerNetWorthHistory); getViewElement().setOnAction(SummaryActions.NEXT_WEEK, () -> { exchange.advance(); }); exchange.weekProperty().addListener((observable, o, n) -> { getViewElement().setWeek((Integer) n); - playerNetWorthHistory.add(player.getNetWorth().floatValue()); - getViewElement().updateChart(playerNetWorthHistory); + playerNetWorthHistory.add(player.getNetWorth()); + getViewElement().updateChart(playerNetWorthHistory.stream().map(BigDecimal::floatValue).toList()); getViewElement().setBalance(player.getMoney().floatValue(), player.getNetWorth().floatValue()); getViewElement().setStatus(player.getStatus()); }); @@ -61,6 +64,40 @@ protected void initInteractions() { }); } + /** + * Synchronizes the controller pointers with the active game engine state references. + * + * @param criticalExchange the current active exchange engine. + * @param activePlayer the current active player context profile. + */ + public void handleContextUpdate(final Exchange criticalExchange, final Player activePlayer) { + if (criticalExchange == null || activePlayer == null) { + return; + } + this.exchange = criticalExchange; + this.player = activePlayer; + + if (this.playerNetWorthHistory == null) { + this.playerNetWorthHistory = new ArrayList<>(); + } + this.playerNetWorthHistory.clear(); + + // 1. Recover history from the updated player instance if available + if (this.player.getNetWorthHistory() != null && !this.player.getNetWorthHistory().isEmpty()) { + this.playerNetWorthHistory.addAll(this.player.getNetWorthHistory()); + } else { + // Safe fallback trajectory baseline points + this.playerNetWorthHistory.add(this.player.getStartingMoney()); + this.playerNetWorthHistory.add(this.player.getNetWorth()); + } + + // Flush updates directly into view layer + getViewElement().setWeek(this.exchange.getWeek()); + getViewElement().updateChart(this.playerNetWorthHistory.stream().map(BigDecimal::floatValue).toList()); + getViewElement().setBalance(this.player.getMoney().floatValue(), this.player.getNetWorth().floatValue()); + } + + /** * Handles save-related events. * @@ -79,30 +116,20 @@ public void handleEvent(final EventData data) { 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()); + + playerNetWorthHistory.clear(); + if (!player.getNetWorthHistory().isEmpty()) { + playerNetWorthHistory.addAll(player.getNetWorthHistory()); } else { - playerNetWorthHistory.add(player.getNetWorth().floatValue()); + playerNetWorthHistory.add(player.getNetWorth()); } + getViewElement().updateChart(playerNetWorthHistory.stream().map(BigDecimal::floatValue).toList()); + getViewElement().setBalance(player.getMoney().floatValue(), player.getNetWorth().floatValue()); getViewElement().setWeek(exchange.getWeek()); - getViewElement().updateChart(playerNetWorthHistory); - getViewElement().setBalance(player.getMoney().floatValue(), - player.getNetWorth().floatValue()); getViewElement().setStatus(player.getStatus()); } } 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 7af130d..b8ab2dc 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 @@ -25,13 +25,13 @@ public class MarketController extends ViewController implements EventSubscriber { /** The {@link Player} owning shares displayed in the market. */ - private final Player player; + private Player player; /** The {@link Exchange} the market is connected to. */ - private final Exchange exchange; + private Exchange exchange; /** Stocks shown in the market grid. */ - private final List stockList; + private List stockList; /** * Constructor. @@ -112,4 +112,25 @@ private int ownedQuantity(final String symbol) { .reduce(BigDecimal.ZERO, BigDecimal::add) .intValue(); } + + /** + * Refreshes the view element with updated context. + * + * @param updatedExchange the current active exchange. + * @param updatedPlayer the current active player. + * @param freshStocks the new active collection of stocks. + */ + public void handleStockPoolUpdate(final Exchange updatedExchange, + final Player updatedPlayer, + final List freshStocks) { + if (updatedExchange == null || updatedPlayer == null || freshStocks == null) { + return; + } + + this.exchange = updatedExchange; + this.player = updatedPlayer; + this.stockList = freshStocks; + + getViewElement().updateStockPool(freshStocks); + } } diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/market/MarketView.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/market/MarketView.java index 4cef923..c6750d7 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/market/MarketView.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/market/MarketView.java @@ -376,4 +376,14 @@ private Polyline buildMiniChart(final Stock stock) { line.getStyleClass().addAll("market-mini-chart", up ? "up" : "down"); return line; } + + /** + * Re-renders the ui with a list of updated stocks. + * + * @param updatedStocks the fresh list of stocks parsed from the save file. + */ + public void updateStockPool(final List updatedStocks) { + this.stockList = (updatedStocks == null) ? new ArrayList<>() : new ArrayList<>(updatedStocks); + renderStocks(); + } } diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/minigames/games/FindStockGame.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/minigames/games/FindStockGame.java index c70ca90..5e4c4f3 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/minigames/games/FindStockGame.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/minigames/games/FindStockGame.java @@ -41,7 +41,7 @@ public final class FindStockGame /** * List of stock symbols to generate selection from. * */ - private final List availableSymbols; + private List availableSymbols; /** * The target stocks' symbol. @@ -125,6 +125,21 @@ private void handleChoice(final String chosen) { generateRound(); } + /** + * Updates the pool of available stock symbols used by the mini-game + * and immediately starts a new round with the updated choices. + * + * @param newSymbols the new list of stock symbols to use. + */ + public void updateStockPool(final List newSymbols) { + if (newSymbols == null) { + return; + } + this.availableSymbols = List.copyOf(newSymbols); + + generateRound(); + } + @Override protected void initLayout() { targetLabel = new Label(); 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 d2a7316..ac4d39e 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 @@ -37,10 +37,10 @@ public class StatsController extends ViewController implements EventSubscriber { /** The {@link Player} whose state is displayed. */ - private final Player player; + private Player player; /** The {@link Exchange} that drives the week-by-week simulation. */ - private final Exchange exchange; + private Exchange exchange; /** * Snapshot of the players net worth at each recorded week. @@ -119,14 +119,11 @@ public void handleEvent(final EventData data) { 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()); + + // STATE_RESET block + balanceHistory.clear(); + if (player.getNetWorthHistory() != null && !player.getNetWorthHistory().isEmpty()) { + balanceHistory.addAll(player.getNetWorthHistory()); } else { balanceHistory.add(player.getStartingMoney()); balanceHistory.add(player.getNetWorth()); @@ -205,4 +202,33 @@ private static final class Aggregate { this.stock = stock; } } + + /** + * Updates the graphs and information of the page given a new exchange and player. + * + * @param updatedExchange the current active exchange engine. + * @param updatedPlayer the current active player context profile. + */ + public void handleContextUpdate(final Exchange updatedExchange, final Player updatedPlayer) { + if (updatedExchange == null || updatedPlayer == null) { + return; + } + + this.exchange = updatedExchange; + this.player = updatedPlayer; + + if (this.balanceHistory == null) { + this.balanceHistory = new ArrayList<>(); + } + + this.balanceHistory.clear(); + + if (!this.player.getNetWorthHistory().isEmpty()) { + this.balanceHistory.addAll(this.player.getNetWorthHistory()); + } else { + this.balanceHistory.add(this.player.getStartingMoney()); + this.balanceHistory.add(this.player.getNetWorth()); + } + pushSnapshot(); + } } \ No newline at end of file diff --git a/src/main/resources/saves/Halleluja.json b/src/main/resources/saves/Halleluja.json index 6068147..7737bf7 100644 --- a/src/main/resources/saves/Halleluja.json +++ b/src/main/resources/saves/Halleluja.json @@ -2,5 +2,12 @@ "name": "Halleluja", "balance": 1000650901.43, "startingCapital": 10000.0, - "stockDataPath": null + "stockDataPath": null, + "week": 1, + "ownedShares": [], + "transactions": [], + "stocks": [ + { "symbol": "TOTA", "name": "Total badass1", "price": 136.80 }, + { "symbol": "LIBR", "name": "Libra avenger of worlds", "price": 19134.23 } + ] } diff --git a/src/main/resources/saves/Newbie.json b/src/main/resources/saves/Newbie.json index ce4262a..2ea4a42 100644 --- a/src/main/resources/saves/Newbie.json +++ b/src/main/resources/saves/Newbie.json @@ -18,5 +18,163 @@ { "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 } + ], + "stocks": [ + { "symbol": "CARR", "name": "Carrier Global", "price": 67.95 }, + { "symbol": "AAPL", "name": "Apple Inc.", "price": 258.10 }, + { "symbol": "SNDK", "name": "Sandisk Corporation", "price": 634.05 }, + { "symbol": "WYNN", "name": "Wynn Resorts", "price": 117.80 }, + { "symbol": "TSCO", "name": "Tractor Supply", "price": 51.12 }, + { "symbol": "AMGN", "name": "Amgen", "price": 329.74 }, + { "symbol": "TSLA", "name": "Tesla Inc.", "price": 376.58 }, + { "symbol": "GDDY", "name": "GoDaddy", "price": 92.35 }, + { "symbol": "SBUX", "name": "Starbucks", "price": 99.41 }, + { "symbol": "KVUE", "name": "Kenvue", "price": 17.97 }, + { "symbol": "META", "name": "Meta Platforms", "price": 756.48 }, + { "symbol": "DLTR", "name": "Dollar Tree", "price": 144.19 }, + { "symbol": "ABBV", "name": "AbbVie", "price": 251.10 }, + { "symbol": "HUBB", "name": "Hubbell Incorporated", "price": 548.39 }, + { "symbol": "JKHY", "name": "Jack Henry & Associates", "price": 180.51 }, + { "symbol": "FSLR", "name": "First Solar", "price": 206.22 }, + { "symbol": "FTNT", "name": "Fortinet", "price": 92.09 }, + { "symbol": "EPAM", "name": "EPAM Systems", "price": 199.46 }, + { "symbol": "POOL", "name": "Pool Corporation", "price": 284.22 }, + { "symbol": "MCHP", "name": "Microchip Technology", "price": 72.57 }, + { "symbol": "VRSK", "name": "Verisk Analytics", "price": 169.99 }, + { "symbol": "MRNA", "name": "Moderna", "price": 33.00 }, + { "symbol": "HOOD", "name": "Robinhood Markets Inc.", "price": 64.62 }, + { "symbol": "VRSN", "name": "Verisign", "price": 247.98 }, + { "symbol": "AMAT", "name": "Applied Materials", "price": 379.06 }, + { "symbol": "MDLZ", "name": "Mondelez International", "price": 66.56 }, + { "symbol": "PCAR", "name": "Paccar", "price": 128.11 }, + { "symbol": "NDAQ", "name": "Nasdaq Inc.", "price": 79.41 }, + { "symbol": "INTU", "name": "Intuit", "price": 403.44 }, + { "symbol": "HSIC", "name": "Henry Schein", "price": 83.94 }, + { "symbol": "FISV", "name": "Fiserv Inc.", "price": 65.43 }, + { "symbol": "CSGP", "name": "CoStar Group", "price": 48.87 }, + { "symbol": "DDOG", "name": "Datadog", "price": 129.43 }, + { "symbol": "BLDR", "name": "Builders FirstSource", "price": 118.27 }, + { "symbol": "AXON", "name": "Axon Enterprise", "price": 473.33 }, + { "symbol": "ALGN", "name": "Align Technology", "price": 187.51 }, + { "symbol": "FICO", "name": "Fair Isaac", "price": 1242.10 }, + { "symbol": "FITB", "name": "Fifth Third Bancorp", "price": 50.53 }, + { "symbol": "NTAP", "name": "NetApp", "price": 105.53 }, + { "symbol": "FOXA", "name": "Fox Corporation (Class A)", "price": 60.47 }, + { "symbol": "QCOM", "name": "Qualcomm", "price": 152.25 }, + { "symbol": "VTRS", "name": "Viatris", "price": 16.25 }, + { "symbol": "LRCX", "name": "Lam Research", "price": 239.39 }, + { "symbol": "PLTR", "name": "Palantir Technologies", "price": 132.98 }, + { "symbol": "PANW", "name": "Palo Alto Networks", "price": 170.52 }, + { "symbol": "NFLX", "name": "Netflix", "price": 75.58 }, + { "symbol": "KLAC", "name": "KLA Corporation", "price": 1342.60 }, + { "symbol": "NVDA", "name": "Nvidia", "price": 187.67 }, + { "symbol": "IBKR", "name": "Interactive Brokers Group", "price": 74.25 }, + { "symbol": "CRWD", "name": "CrowdStrike", "price": 448.16 }, + { "symbol": "ULTA", "name": "Ulta Beauty", "price": 577.29 }, + { "symbol": "JBHT", "name": "J.B. Hunt", "price": 225.21 }, + { "symbol": "SMCI", "name": "Supermicro", "price": 31.69 }, + { "symbol": "NXPI", "name": "NXP Semiconductors", "price": 258.77 }, + { "symbol": "VRTX", "name": "Vertex Pharmaceuticals", "price": 456.77 }, + { "symbol": "MTCH", "name": "Match Group", "price": 29.70 }, + { "symbol": "CBOE", "name": "Cboe Global Markets", "price": 261.74 }, + { "symbol": "CPRT", "name": "Copart", "price": 38.12 }, + { "symbol": "VICI", "name": "Vici Properties", "price": 33.48 }, + { "symbol": "CHRW", "name": "C.H. Robinson", "price": 182.86 }, + { "symbol": "INTC", "name": "Intel", "price": 44.51 }, + { "symbol": "ROST", "name": "Ross Stores", "price": 206.42 }, + { "symbol": "GEHC", "name": "GE HealthCare", "price": 86.95 }, + { "symbol": "SCHW", "name": "Charles Schwab Corporation", "price": 96.49 }, + { "symbol": "CVNA", "name": "Carvana Co.", "price": 368.51 }, + { "symbol": "IDXX", "name": "Idexx Laboratories", "price": 766.54 }, + { "symbol": "INCY", "name": "Incyte", "price": 101.02 }, + { "symbol": "GNRC", "name": "Generac", "price": 224.22 }, + { "symbol": "CPAY", "name": "Corpay", "price": 366.15 }, + { "symbol": "REGN", "name": "Regeneron Pharmaceuticals", "price": 833.09 }, + { "symbol": "CTAS", "name": "Cintas", "price": 230.80 }, + { "symbol": "FAST", "name": "Fastenal", "price": 42.07 }, + { "symbol": "AMZN", "name": "Amazon", "price": 207.86 }, + { "symbol": "ZBRA", "name": "Zebra Technologies", "price": 285.78 }, + { "symbol": "ODFL", "name": "Old Dominion", "price": 208.16 }, + { "symbol": "TTWO", "name": "Take-Two Interactive", "price": 176.58 }, + { "symbol": "CTRA", "name": "Coterra", "price": 28.69 }, + { "symbol": "LDOS", "name": "Leidos", "price": 172.90 }, + { "symbol": "ARES", "name": "Ares Management Corporation", "price": 131.22 }, + { "symbol": "CINF", "name": "Cincinnati Financial", "price": 172.93 }, + { "symbol": "ANET", "name": "Arista Networks", "price": 142.22 }, + { "symbol": "AMCR", "name": "Amcor", "price": 45.14 }, + { "symbol": "HBAN", "name": "Huntington Bancshares", "price": 20.67 }, + { "symbol": "EVRG", "name": "Evergy", "price": 75.68 }, + { "symbol": "ABNB", "name": "Airbnb", "price": 125.39 }, + { "symbol": "DASH", "name": "DoorDash", "price": 164.09 }, + { "symbol": "COIN", "name": "Coinbase Global", "price": 146.37 }, + { "symbol": "CIEN", "name": "Ciena Corporation", "price": 254.65 }, + { "symbol": "FANG", "name": "Diamondback Energy", "price": 155.78 }, + { "symbol": "PSKY", "name": "Paramount Skydance Corp", "price": 10.73 }, + { "symbol": "ORLY", "name": "O'Reilly Auto Parts", "price": 96.40 }, + { "symbol": "SBAC", "name": "SBA Communications", "price": 193.40 }, + { "symbol": "ACGL", "name": "Arch Capital Group", "price": 103.61 }, + { "symbol": "CTSH", "name": "Cognizant", "price": 60.84 }, + { "symbol": "VLTO", "name": "Veralto", "price": 102.80 }, + { "symbol": "MPWR", "name": "Monolithic Power Systems", "price": 1210.08 }, + { "symbol": "PODD", "name": "Insulet Corporation", "price": 264.60 }, + { "symbol": "MRSH", "name": "Marsh & McLennan Companies Inc.", "price": 159.69 }, + { "symbol": "PAYC", "name": "Paycom", "price": 112.44 }, + { "symbol": "NTRS", "name": "Northern Trust", "price": 138.26 }, + { "symbol": "ADSK", "name": "Autodesk", "price": 232.08 }, + { "symbol": "MSCI", "name": "MSCI", "price": 472.01 }, + { "symbol": "ORCL", "name": "Oracle Corporation", "price": 168.14 }, + { "symbol": "ERIE", "name": "Erie Indemnity", "price": 273.98 }, + { "symbol": "TECH", "name": "Bio-Techne", "price": 59.22 }, + { "symbol": "TRMB", "name": "Trimble Inc.", "price": 70.40 }, + { "symbol": "EBAY", "name": "eBay", "price": 67.25 }, + { "symbol": "INVH", "name": "Invitation Homes", "price": 26.09 }, + { "symbol": "NDSN", "name": "Nordson Corporation", "price": 289.84 }, + { "symbol": "DECK", "name": "Deckers Brands", "price": 114.76 }, + { "symbol": "ADBE", "name": "Adobe Inc.", "price": 258.37 }, + { "symbol": "SNPS", "name": "Synopsys", "price": 452.44 }, + { "symbol": "CHTR", "name": "Charter Communications", "price": 270.87 }, + { "symbol": "STLD", "name": "Steel Dynamics", "price": 193.65 }, + { "symbol": "BIIB", "name": "Biogen", "price": 193.61 }, + { "symbol": "TRGP", "name": "Targa Resources", "price": 242.36 }, + { "symbol": "SOLV", "name": "Solventum", "price": 70.47 }, + { "symbol": "TROW", "name": "T. Rowe Price", "price": 92.55 }, + { "symbol": "AVGO", "name": "Broadcom", "price": 365.11 }, + { "symbol": "CSCO", "name": "Cisco", "price": 75.74 }, + { "symbol": "CTVA", "name": "Corteva", "price": 76.63 }, + { "symbol": "EXPD", "name": "Expeditors International", "price": 169.73 }, + { "symbol": "EXPE", "name": "Expedia Group", "price": 281.69 }, + { "symbol": "AKAM", "name": "Akamai Technologies", "price": 85.09 }, + { "symbol": "CBRE", "name": "CBRE Group", "price": 137.79 }, + { "symbol": "FFIV", "name": "F5 Inc.", "price": 285.06 }, + { "symbol": "MSFT", "name": "Microsoft", "price": 389.46 }, + { "symbol": "COST", "name": "Costco", "price": 932.30 }, + { "symbol": "NCLH", "name": "Norwegian Cruise Line Holdings", "price": 24.07 }, + { "symbol": "KEYS", "name": "Keysight Technologies", "price": 207.44 }, + { "symbol": "SPGI", "name": "S&P Global", "price": 426.67 }, + { "symbol": "UBER", "name": "Uber", "price": 75.84 }, + { "symbol": "BKNG", "name": "Booking Holdings", "price": 4386.71 }, + { "symbol": "DELL", "name": "Dell Technologies", "price": 142.26 }, + { "symbol": "DXCM", "name": "Dexcom", "price": 65.13 }, + { "symbol": "PYPL", "name": "PayPal", "price": 32.31 }, + { "symbol": "GOOG", "name": "Alphabet Inc. (Class C)", "price": 282.90 }, + { "symbol": "BALL", "name": "Ball Corporation", "price": 60.67 }, + { "symbol": "WELL", "name": "Welltower", "price": 194.82 }, + { "symbol": "MNST", "name": "Monster Beverage", "price": 80.45 }, + { "symbol": "OTIS", "name": "Otis Worldwide", "price": 84.53 }, + { "symbol": "SWKS", "name": "Skyworks Solutions", "price": 63.13 }, + { "symbol": "GRMN", "name": "Garmin", "price": 185.49 }, + { "symbol": "WDAY", "name": "Workday Inc.", "price": 118.22 }, + { "symbol": "APTV", "name": "Aptiv", "price": 83.68 }, + { "symbol": "RVTY", "name": "Revvity", "price": 91.38 }, + { "symbol": "TMUS", "name": "T-Mobile US", "price": 204.96 }, + { "symbol": "LULU", "name": "Lululemon Athletica", "price": 167.54 }, + { "symbol": "HOLX", "name": "Hologic", "price": 64.16 }, + { "symbol": "NWSA", "name": "News Corp (Class A)", "price": 21.24 }, + { "symbol": "PAYX", "name": "Paychex", "price": 89.52 }, + { "symbol": "CDNS", "name": "Cadence Design Systems", "price": 295.12 }, + { "symbol": "ALLE", "name": "Allegion", "price": 198.52 }, + { "symbol": "GILD", "name": "Gilead Sciences", "price": 144.57 }, + { "symbol": "EQIX", "name": "Equinix", "price": 932.33 }, + { "symbol": "ISRG", "name": "Intuitive Surgical", "price": 590.38 } ] } diff --git a/src/main/resources/saves/Tommy.json b/src/main/resources/saves/Tommy.json deleted file mode 100644 index 90979ce..0000000 --- a/src/main/resources/saves/Tommy.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "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 } - ] -}