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); - ListDelegates 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 ListUsed 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* 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- * 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- * 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- * Listens for {@link EventType#LOAD_SAVE} events. When one fires the - * service: - *
- *- * 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 MapUsed 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- * 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- * 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- * 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- * 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- * 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- * 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- * 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