From 60c219148011c0b277a27920554195ed858b59b9 Mon Sep 17 00:00:00 2001 From: Elisabeth Berg Date: Mon, 25 May 2026 14:00:46 +0200 Subject: [PATCH] Improve Javadocs Make Javadocs more accurate and descriptive --- checkstyle.xml | 101 ++ pom.xml | 21 + .../java/Controller/StockFileHandler.java | 69 +- src/main/java/Model/Exchange.java | 132 +- src/main/java/Model/ExchangeObserver.java | 8 +- src/main/java/Model/Player.java | 43 +- src/main/java/Model/PlayerStatus.java | 23 +- src/main/java/Model/Portfolio.java | 47 +- src/main/java/Model/Purchase.java | 19 +- src/main/java/Model/PurchaseCalculator.java | 34 +- src/main/java/Model/Sale.java | 19 +- src/main/java/Model/SaleCalculator.java | 37 +- src/main/java/Model/Share.java | 28 +- src/main/java/Model/Stock.java | 36 +- src/main/java/Model/Transaction.java | 40 +- src/main/java/Model/TransactionArchive.java | 51 +- .../java/Model/TransactionCalculator.java | 25 +- src/main/java/Model/TransactionFactory.java | 15 +- src/main/java/View/GameSetupScene.java | 10 +- src/main/java/View/Launcher.java | 9 +- src/main/java/View/MainGameScene.java | 1158 +++++++++-------- src/main/java/View/StockTradingGameApp.java | 25 +- target/test-classes/ExchangeTest.class | Bin 10733 -> 10397 bytes target/test-classes/PortfolioTest.class | Bin 5806 -> 5661 bytes target/test-classes/ShareTest.class | Bin 4255 -> 4137 bytes target/test-classes/StockTest.class | Bin 6225 -> 6116 bytes 26 files changed, 1300 insertions(+), 650 deletions(-) create mode 100644 checkstyle.xml diff --git a/checkstyle.xml b/checkstyle.xml new file mode 100644 index 0000000..9f86503 --- /dev/null +++ b/checkstyle.xml @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pom.xml b/pom.xml index a74b899..1b52053 100644 --- a/pom.xml +++ b/pom.xml @@ -70,6 +70,27 @@ maven-javadoc-plugin 3.12.0 + + + + org.apache.maven.plugins + maven-checkstyle-plugin + 3.3.1 + + checkstyle.xml + UTF-8 + true + false + false + + + + com.puppycrawl.tools + checkstyle + 10.12.5 + + + \ No newline at end of file diff --git a/src/main/java/Controller/StockFileHandler.java b/src/main/java/Controller/StockFileHandler.java index b64f9c0..bf808a7 100644 --- a/src/main/java/Controller/StockFileHandler.java +++ b/src/main/java/Controller/StockFileHandler.java @@ -1,37 +1,58 @@ package Controller; -import java.nio.file.*; + +import Model.Stock; import java.io.IOException; import java.math.BigDecimal; +import java.nio.file.*; import java.util.ArrayList; import java.util.List; -import Model.Stock; +/** + * Handles loading and saving Stock data from/to CSV files. + * The CSV format is: ticker,company_name,price + * Lines starting with '#' are treated as comments and ignored. + */ public class StockFileHandler { - // lesing - public List loadStocksFromFile(String filename) throws IOException { - return Files.readAllLines(Paths.get(filename)).stream() - .map(String::trim) - .filter(line -> !line.isEmpty() && !line.startsWith("#")) - .map(line -> line.split(",")) - .filter(parts -> parts.length == 3) - .map(parts -> new Stock(parts[0], parts[1], new BigDecimal(parts[2]))) - .toList(); - } + /** + * Loads stocks from a CSV file. + * Expected format: ticker,company,price + * Empty lines and lines starting with '#' are ignored. + * + * @param filename the path to the CSV file + * @return a list of Stock objects loaded from the file + * @throws IOException if the file cannot be read + */ + public List loadStocksFromFile(String filename) throws IOException { + return Files.readAllLines(Paths.get(filename)).stream() + .map(String::trim) + .filter(line -> !line.isEmpty() && !line.startsWith("#")) + .map(line -> line.split(",")) + .filter(parts -> parts.length == 3) + .map(parts -> new Stock(parts[0], parts[1], new BigDecimal(parts[2]))) + .toList(); + } - // lagring - public void saveStocksToFile(String filename, List stocks) throws IOException { - List lines = new ArrayList<>(); - lines.add("# Ticker,Name,Price"); + /** + * Saves stocks to a CSV file. + * The file includes a header comment line followed by one stock per line. + * + * @param filename the path to the CSV file to write + * @param stocks the list of stocks to save + * @throws IOException if the file cannot be written + */ + public void saveStocksToFile(String filename, List stocks) throws IOException { + List lines = new ArrayList<>(); + lines.add("# Ticker,Name,Price"); - lines.addAll(stocks.stream() - .map(stock -> String.format("%s,%s,%s", - stock.getSymbol(), - stock.getCompany(), - stock.getSalesPrice().toString())) - .toList()); + lines.addAll(stocks.stream() + .map(stock -> String.format("%s,%s,%s", + stock.getSymbol(), + stock.getCompany(), + stock.getSalesPrice().toString())) + .toList()); - Files.write(Paths.get(filename), lines); - } + Files.write(Paths.get(filename), lines); + } } \ No newline at end of file diff --git a/src/main/java/Model/Exchange.java b/src/main/java/Model/Exchange.java index 753e980..7feb4f2 100644 --- a/src/main/java/Model/Exchange.java +++ b/src/main/java/Model/Exchange.java @@ -8,7 +8,10 @@ import java.util.Random; /** - * Exchange class. + * Represents a stock exchange where players can buy and sell stocks. + * The Exchange maintains a collection of stocks, advances through weeks, + * and executes trades. It implements the Observer pattern to notify listeners + * (typically the UI) whenever its state changes. */ public class Exchange { @@ -20,6 +23,13 @@ public class Exchange { // Registered observers private final List observers = new ArrayList<>(); + /** + * Constructs a new Exchange with the given name and initial stocks. + * + * @param name the exchange name. Cannot be null or blank + * @param stocks the initial list of stocks. Cannot be null or contain null entries + * @throws IllegalArgumentException if any parameter is invalid + */ public Exchange(String name, List stocks) { if (name == null || name.isBlank()) { throw new IllegalArgumentException("Exchange name cannot be null or blank"); @@ -43,6 +53,12 @@ public Exchange(String name, List stocks) { // ---- Observer ---- + /** + * Registers an observer to receive notifications of state changes. + * + * @param observer the observer to register. Cannot be null + * @throws IllegalArgumentException if observer is null + */ public void addObserver(ExchangeObserver observer) { if (observer == null) { throw new IllegalArgumentException("Observer cannot be null"); @@ -52,24 +68,49 @@ public void addObserver(ExchangeObserver observer) { } } + /** + * Unregisters an observer from receiving notifications. + * + * @param observer the observer to remove + */ public void removeObserver(ExchangeObserver observer) { observers.remove(observer); } + /** + * Notifies all registered observers of a state change. + */ private void notifyObservers() { for (ExchangeObserver observer : observers) { observer.onExchangeUpdated(this); } } + /** + * Returns the exchange name. + * + * @return the name + */ public String getName() { return name; } + /** + * Returns the current week number. + * + * @return the week + */ public int getWeek() { return week; } + /** + * Checks if a stock with the given symbol is available on this exchange. + * + * @param symbol the stock symbol. Cannot be null or blank + * @return true if the stock exists; false otherwise + * @throws IllegalArgumentException if symbol is invalid + */ public boolean hasStock(String symbol) { if (symbol == null || symbol.isBlank()) { throw new IllegalArgumentException("Symbol cannot be null or blank"); @@ -77,6 +118,13 @@ public boolean hasStock(String symbol) { return stockMap.containsKey(symbol); } + /** + * Returns the stock with the given symbol. + * + * @param symbol the stock symbol. Cannot be null or blank + * @return the Stock, or null if not found + * @throws IllegalArgumentException if symbol is invalid + */ public Stock getStock(String symbol) { if (symbol == null || symbol.isBlank()) { throw new IllegalArgumentException("Symbol cannot be null or blank"); @@ -84,6 +132,13 @@ public Stock getStock(String symbol) { return stockMap.get(symbol); } + /** + * Searches for stocks matching the search term by symbol or company name. + * + * @param searchTerm the search term. Cannot be null + * @return a list of matching stocks (case-insensitive) + * @throws IllegalArgumentException if searchTerm is null + */ public List findStocks(String searchTerm) { if (searchTerm == null) { throw new IllegalArgumentException("Search term cannot be null"); @@ -101,6 +156,16 @@ public List findStocks(String searchTerm) { return result; } + /** + * Executes a buy transaction for the specified stock and quantity. + * + * @param symbol the stock symbol. Cannot be null or blank + * @param quantity the number of shares to buy. Must be greater than zero + * @param player the player executing the purchase. Cannot be null + * @return the completed Purchase transaction + * @throws IllegalArgumentException if any parameter is invalid or stock not found + * @throws IllegalStateException if player has insufficient funds + */ public Transaction buy(String symbol, BigDecimal quantity, Player player) { if (symbol == null || symbol.isBlank()) { throw new IllegalArgumentException("Symbol cannot be null or blank"); @@ -120,7 +185,7 @@ public Transaction buy(String symbol, BigDecimal quantity, Player player) { throw new IllegalArgumentException("No stock found with symbol: " + symbol); } - // lager en ny "andel" basert på nåværende salgspris + // Creates a new share based on current selling price Share shareToBuy = new Share(stock, quantity, stock.getSalesPrice()); Purchase purchase = new Purchase(shareToBuy, this.week); purchase.commit(player); @@ -130,19 +195,40 @@ public Transaction buy(String symbol, BigDecimal quantity, Player player) { return purchase; } + /** + * Executes a full sale transaction for all shares in the given holding. + * + * @param share the share to sell completely. Cannot be null + * @param player the player executing the sale. Cannot be null + * @return the completed Sale transaction + * @throws IllegalArgumentException if any parameter is invalid + * @throws IllegalStateException if player doesn't own the share + */ public Transaction sell(Share share, Player player) { return sell(share, share.getQuantity(), player); } + /** + * Executes a partial or complete sale transaction. + * If the sell quantity is less than the share quantity, creates a partial sale + * and leaves the remainder in the portfolio. + * + * @param originalShare the share to sell from. Cannot be null + * @param sellQuantity the quantity to sell. Cannot exceed the original quantity + * @param player the player executing the sale. Cannot be null + * @return the completed Sale transaction + * @throws IllegalArgumentException if any parameter is invalid + * @throws IllegalStateException if sell quantity exceeds holding + */ public Transaction sell(Share originalShare, BigDecimal sellQuantity, Player player) { if (originalShare == null || sellQuantity == null) { throw new IllegalArgumentException("Share and quantity cannot be null"); } if (player == null) { - throw new IllegalArgumentException("Player cannot be null"); + throw new IllegalArgumentException("Player cannot be null"); } - // Kan ikke selge mer enn man eier + // Cannot sell more shares than you have if (sellQuantity.compareTo(originalShare.getQuantity()) > 0) { throw new IllegalArgumentException("Cannot sell more shares than owned"); } @@ -153,8 +239,8 @@ public Transaction sell(Share originalShare, BigDecimal sellQuantity, Player pla if (sellQuantity.compareTo(originalShare.getQuantity()) < 0) { - //Delsalg: - // Original mengde - mengde som selges = gjenværende mengde + // Partial sale: + // Original quantity - quantity sold = remaining quantity player.getPortfolio().removeShare(originalShare); @@ -164,7 +250,7 @@ public Transaction sell(Share originalShare, BigDecimal sellQuantity, Player pla .getStock(), remainderShare, originalShare.getPurchasePrice())); - //Legger delmengden midlertidig til i portfolio slik at Sale.commit() finner den + // Temporarily adds the subset to the portfolio so that Sale.commit() can find it shareToSell = new Share(originalShare .getStock(), sellQuantity, originalShare.getPurchasePrice()); @@ -174,11 +260,11 @@ public Transaction sell(Share originalShare, BigDecimal sellQuantity, Player pla } else { - // Fullstendig salg: hele andelen selges som normalt + // Complete sale: the entire share is sold as normal shareToSell = originalShare; } - // Salgstransaksjon + // Salestransaction Transaction sale = TransactionFactory.createSale(shareToSell, this.week); sale.commit(player); @@ -187,15 +273,17 @@ public Transaction sell(Share originalShare, BigDecimal sellQuantity, Player pla } /** - * Advances to the next week. + * Advances the game to the next week and updates all stock prices. + * Each stock's price is randomly adjusted by -10% to +10% per week. + * Notifies all observers of the change. */ public void advance() { week++; - for (Stock stock : stockMap.values()) { // henter stock-objektene + for (Stock stock : stockMap.values()) { // Retrieves the stock-objects - BigDecimal currentPrice = stock.getSalesPrice(); // henter siste pris fra Stock + BigDecimal currentPrice = stock.getSalesPrice(); // Retrieves last price from Stock double changePercent = (random.nextDouble() - 0.5) * 0.1; @@ -203,7 +291,7 @@ public void advance() { BigDecimal newPrice = currentPrice.add(change); - if (newPrice.compareTo(BigDecimal.ZERO) > 0) { // unngå negativ pris + if (newPrice.compareTo(BigDecimal.ZERO) > 0) { // avoid negative price stock.addNewSalesPrice(newPrice); } } @@ -212,12 +300,13 @@ public void advance() { } /** - * Shows the weeks Gainers, the stocks with the best price change. + * Returns the top-performing stocks (gainers) for the current week. + * Gainers are stocks with the highest positive price change. * - * @param limit how many gainers - * @return returns the gainers + * @param limit the maximum number of gainers to return + * @return a list of the top gainers, sorted by price change (highest first) */ - public List getGainers(int limit) { // viser "vinnerne" + public List getGainers(int limit) { // shows the "winners" return stockMap.values().stream() .sorted((s1, s2) -> s2.getLatestPriceChange().compareTo(s1.getLatestPriceChange())) .limit(limit) @@ -225,12 +314,13 @@ public List getGainers(int limit) { // viser "vinnerne" } /** - * Shows the weeks Losers, the stocks with the worst price change. + * Returns the lowest-performing stocks (losers) for the current week. + * Losers are stocks with the highest negative price change. * - * @param limit how many losers - * @return returns the losers + * @param limit the maximum number of losers to return + * @return a list of the top losers, sorted by price change (lowest first) */ - public List getLosers(int limit) { // viser "taperne" + public List getLosers(int limit) { // shows the "losers" return stockMap.values().stream() .sorted((s1, s2) -> s1.getLatestPriceChange().compareTo(s2.getLatestPriceChange())) .limit(limit) diff --git a/src/main/java/Model/ExchangeObserver.java b/src/main/java/Model/ExchangeObserver.java index 145f3cc..7cef281 100644 --- a/src/main/java/Model/ExchangeObserver.java +++ b/src/main/java/Model/ExchangeObserver.java @@ -1,14 +1,14 @@ package Model; /** - * Observer interface for the Exchange subject. - * Implement this interface to receive notifications whenever the exchange - * state changes (week advances, a trade is committed). + * Observer interface for receiving notifications from the Exchange. + * Implementations are notified whenever the Exchange state changes, + * allowing the View and other components to stay synchronized. */ public interface ExchangeObserver { /** - * Called by the Exchange after its state has changed. + * Called by the Exchange when its state changes. * * @param exchange the Exchange that triggered the notification */ diff --git a/src/main/java/Model/Player.java b/src/main/java/Model/Player.java index 3293db1..dd9d81e 100644 --- a/src/main/java/Model/Player.java +++ b/src/main/java/Model/Player.java @@ -3,8 +3,10 @@ import java.math.BigDecimal; /** - * The player class is a description of the person player the game, - * their starting point and their progress. + * Represents a player in the stock trading game. + * A Player manages their personal portfolio of shares, cash balance, and transaction + * history. Players can buy and sell stocks, and their performance is tracked through + * status levels based on trading activity and net worth growth. */ public class Player { private String name; @@ -14,10 +16,11 @@ public class Player { private TransactionArchive transactionArchive; /** - * Player method that includes the players name and the money they start with. + * Constructs a new Player with the given name and starting capital. * - * @param name the name of the player - * @param startingMoney money the player starts with + * @param name the player's name. Cannot be null or blank + * @param startingMoney the initial cash balance. Cannot be null or negative + * @throws IllegalArgumentException if any parameter is invalid */ public Player(String name, BigDecimal startingMoney) { if (name == null || name.isBlank()) { @@ -37,18 +40,29 @@ public Player(String name, BigDecimal startingMoney) { this.transactionArchive = new TransactionArchive(); } + /** + * Returns the player's name. + * + * @return the player's name + */ public String getName() { return this.name; } + /** + * Returns the player's current cash balance. + * + * @return the current amount of money available + */ public BigDecimal getMoney() { return this.money; } /** - * Amount of money added. + * Adds the specified amount to the player's cash balance. * - * @param amount amount of money + * @param amount the amount to add. Cannot be null or negative + * @throws IllegalArgumentException if amount is invalid */ public void addMoney(BigDecimal amount) { if (amount == null) { @@ -61,9 +75,10 @@ public void addMoney(BigDecimal amount) { } /** - * Amount of money withdrawn. + * Removes the specified amount from the player's cash balance. * - * @param amount amount of money + * @param amount the amount to withdraw. Cannot be null or negative + * @throws IllegalArgumentException if amount is invalid */ public void withdrawMoney(BigDecimal amount) { if (amount == null) { @@ -75,10 +90,20 @@ public void withdrawMoney(BigDecimal amount) { this.money = this.money.subtract(amount); } + /** + * Returns the player's portfolio. + * + * @return the portfolio containing held shares + */ public Portfolio getPortfolio() { return this.portfolio; } + /** + * Returns the player's transaction archive. + * + * @return the archive of all transactions + */ public TransactionArchive getTransactionArchive() { return this.transactionArchive; } diff --git a/src/main/java/Model/PlayerStatus.java b/src/main/java/Model/PlayerStatus.java index e72a9ef..312e38f 100644 --- a/src/main/java/Model/PlayerStatus.java +++ b/src/main/java/Model/PlayerStatus.java @@ -1,12 +1,13 @@ package Model; /** - * Enum representing the player's status level based on trading activity and performance. - - * Status levels: - * - NOVICE: Starting level, no requirements - * - INVESTOR: Traded for at least 10 weeks AND increased net worth by at least 20% - * - SPECULATOR: Traded for at least 20 weeks AND doubled the net worth (100% increase) + * Enum representing the player's trading status level. + * Status levels are determined by trading activity (weeks with transactions) + * and net worth performance compared to starting capital. + * Levels: + * - NOVICE: Initial status (default) + * - INVESTOR: 10+ weeks of trading AND net worth increased by 20% + * - SPECULATOR: 20+ weeks of trading AND net worth doubled */ public enum PlayerStatus { NOVICE("Novice"), @@ -15,10 +16,20 @@ public enum PlayerStatus { private final String displayName; + /** + * Constructs a PlayerStatus with the given display name. + * + * @param displayName the name for UI display + */ PlayerStatus(String displayName) { this.displayName = displayName; } + /** + * Returns the display name for this status. + * + * @return the display name + */ public String getDisplayName() { return displayName; } diff --git a/src/main/java/Model/Portfolio.java b/src/main/java/Model/Portfolio.java index 902b8b9..7c9e38d 100644 --- a/src/main/java/Model/Portfolio.java +++ b/src/main/java/Model/Portfolio.java @@ -5,21 +5,28 @@ import java.util.List; /** - * Portfolio class of the player. + * Represents a portfolio of shares held by a player. + * A Portfolio manages a collection of Share objects and provides methods to add, + * remove, and query holdings. It can calculate the total net worth based on current + * market prices. */ public class Portfolio { private final List shares; + /** + * Constructs a new empty Portfolio. + */ public Portfolio() { this.shares = new ArrayList<>(); } /** - * Method to add share. + * Adds a share to the portfolio. * - * @param share the share being added - * @return returns the added share + * @param share the share to add. Cannot be null + * @return true if the share was added successfully + * @throws IllegalArgumentException if share is null */ public boolean addShare(Share share) { if (share == null) { @@ -29,10 +36,11 @@ public boolean addShare(Share share) { } /** - * Method to remove share. + * Removes a share from the portfolio. * - * @param share the share being removed - * @return returns the removed share + * @param share the share to remove. Cannot be null + * @return true if the share was removed; false if it was not in the portfolio + * @throws IllegalArgumentException if share is null */ public boolean removeShare(Share share) { if (share == null) { @@ -41,15 +49,21 @@ public boolean removeShare(Share share) { return shares.remove(share); } + /** + * Returns a defensive copy of all shares in the portfolio. + * + * @return a copy of the shares list + */ public List getShares() { return new ArrayList<>(shares); } /** - * Method to get all shares. + * Returns all shares in the portfolio for a specific stock symbol. * - * @param symbol the symbol for each stock - * @return the shares of stocks + * @param symbol the stock symbol to filter by. Cannot be null or blank + * @return a list of shares matching the symbol + * @throws IllegalArgumentException if symbol is null or blank */ public List getShares(String symbol) { if (symbol == null || symbol.isBlank()) { @@ -61,10 +75,11 @@ public List getShares(String symbol) { } /** - * Method to check if contains share. + * Checks if the portfolio contains a specific share. * - * @param share the share - * @return the share if not null + * @param share the share to check for. Cannot be null + * @return true if the share is in the portfolio; false otherwise + * @throws IllegalArgumentException if share is null */ public boolean contains(Share share) { if (share == null) { @@ -74,9 +89,11 @@ public boolean contains(Share share) { } /** - * Method to get the new worth. + * Calculates the total net worth of the portfolio based on current market prices. + * The net worth is the sum of all shares valued at their current sales prices, + * minus any transaction costs (commissions and taxes). * - * @return returns the net worth + * @return the total net worth of all holdings */ public BigDecimal getNetWorth() { return shares.stream() diff --git a/src/main/java/Model/Purchase.java b/src/main/java/Model/Purchase.java index d367a7b..b81f655 100644 --- a/src/main/java/Model/Purchase.java +++ b/src/main/java/Model/Purchase.java @@ -3,13 +3,30 @@ import java.math.BigDecimal; /** - * Purchase class whenever the player makes a purchase. + * Represents a stock purchase transaction. + * A Purchase transaction represents a player buying shares. When committed, + * it deducts money from the player's account, adds the shares to their portfolio, + * and records the transaction in the archive. */ public class Purchase extends Transaction { + /** + * Constructs a new Purchase transaction. + * + * @param share the share to purchase. Cannot be null + * @param week the week number when the purchase occurs. Must be at least 1 + */ public Purchase(Share share, int week) { super(share, week, new PurchaseCalculator(share)); } + /** + * Commits this purchase transaction, deducting funds and adding shares to the portfolio. + * + * @param player the player executing the purchase. Cannot be null + * @throws IllegalArgumentException if player is null + * @throws IllegalStateException if purchase has already been committed or if + * the player has insufficient funds + */ @Override public void commit(Player player) { if (player == null) { diff --git a/src/main/java/Model/PurchaseCalculator.java b/src/main/java/Model/PurchaseCalculator.java index e90839f..e2a7482 100644 --- a/src/main/java/Model/PurchaseCalculator.java +++ b/src/main/java/Model/PurchaseCalculator.java @@ -3,34 +3,60 @@ import java.math.BigDecimal; /** - * PurchaseCalculator class that does calculations. + * Calculates costs and totals for purchase transactions. + * A PurchaseCalculator computes the gross cost (quantity × price), applies + * a commission fee, and produces the total transaction cost. */ public class PurchaseCalculator implements TransactionCalculator { private final BigDecimal purchasePrice; private final BigDecimal quantity; - + + /** + * Constructs a PurchaseCalculator for the given share. + * + * @param share the share being purchased. Cannot be null + */ public PurchaseCalculator(Share share) { this.purchasePrice = share.getPurchasePrice(); this.quantity = share.getQuantity(); } + /** + * Calculates the gross cost without fees (quantity × purchase price). + * + * @return the gross cost + */ @Override public BigDecimal calculateGross() { return this.purchasePrice.multiply(this.quantity); } + /** + * Calculates the purchase commission (0.5% of gross). + * + * @return the commission fee + */ @Override public BigDecimal calculateCommission() { BigDecimal rate = new BigDecimal("0.005"); return calculateGross().multiply(rate); } + /** + * Calculates the tax on purchase (always 0 for purchases). + * + * @return zero + */ @Override public BigDecimal calculateTax() { - BigDecimal tax = new BigDecimal("0"); - return tax; + return BigDecimal.ZERO; } + /** + * Calculates the total purchase cost (gross + commission + tax). + * + * @return the total transaction cost + */ @Override public BigDecimal calculateTotal() { return calculateGross().add(calculateCommission()).add(calculateTax()); diff --git a/src/main/java/Model/Sale.java b/src/main/java/Model/Sale.java index e130f41..8b2e05e 100644 --- a/src/main/java/Model/Sale.java +++ b/src/main/java/Model/Sale.java @@ -3,13 +3,30 @@ import java.math.BigDecimal; /** - * Sale class for whenever the player makes a sale. + * Represents a stock sale transaction. + * A Sale transaction represents a player selling shares from their portfolio. + * When committed, it adds money to the player's account, removes the shares from + * their portfolio, and records the transaction in the archive. */ public class Sale extends Transaction { + /** + * Constructs a new Sale transaction. + * + * @param share the share to sell. Cannot be null + * @param week the week number when the sale occurs. Must be at least 1 + */ public Sale(Share share, int week) { super(share, week, new SaleCalculator(share)); } + /** + * Commits this sale transaction, crediting funds and removing shares from the portfolio. + * + * @param player the player executing the sale. Cannot be null + * @throws IllegalArgumentException if player is null + * @throws IllegalStateException if sale has already been committed or if the + * player does not own the share being sold + */ @Override public void commit(Player player) { if (player == null) { diff --git a/src/main/java/Model/SaleCalculator.java b/src/main/java/Model/SaleCalculator.java index 93b6f93..271225a 100644 --- a/src/main/java/Model/SaleCalculator.java +++ b/src/main/java/Model/SaleCalculator.java @@ -3,17 +3,19 @@ import java.math.BigDecimal; /** - * SaleCalculator class that calculates gross, commission, tax and total. + * Calculates costs, taxes, and proceeds for sale transactions. + * A SaleCalculator computes the gross revenue (quantity × sales price), applies + * a commission fee, calculates capital gains tax, and produces the net proceeds. */ -public class SaleCalculator implements TransactionCalculator{ - private BigDecimal purchasePrice; - private BigDecimal salesPrice; - private BigDecimal quantity; +public class SaleCalculator implements TransactionCalculator { + private final BigDecimal purchasePrice; + private final BigDecimal salesPrice; + private final BigDecimal quantity; /** - * Method with parameters. + * Constructs a SaleCalculator for the given share. * - * @param share share from calculator + * @param share the share being sold. Cannot be null */ public SaleCalculator(Share share) { this.purchasePrice = share.getPurchasePrice(); @@ -21,11 +23,21 @@ public SaleCalculator(Share share) { this.quantity = share.getQuantity(); } + /** + * Calculates the gross revenue (quantity × sales price). + * + * @return the gross revenue + */ @Override public BigDecimal calculateGross() { return this.salesPrice.multiply(this.quantity); } + /** + * Calculates the sale commission (1% of gross). + * + * @return the commission fee + */ @Override public BigDecimal calculateCommission() { BigDecimal rate = new BigDecimal("0.01"); @@ -33,8 +45,12 @@ public BigDecimal calculateCommission() { } /** - * Method that calculates tax. + * Calculates the capital gains tax (30% of profit). + * Profit is calculated as: revenue - commission - original purchase cost. + * + * @return the capital gains tax */ + @Override public BigDecimal calculateTax() { BigDecimal sellingCost = this.purchasePrice.multiply(this.quantity); BigDecimal profit = calculateGross().subtract(calculateCommission()).subtract(sellingCost); @@ -42,6 +58,11 @@ public BigDecimal calculateTax() { return profit.multiply(rate); } + /** + * Calculates the net proceeds (gross - commission - tax). + * + * @return the total amount credited to the player + */ @Override public BigDecimal calculateTotal() { return calculateGross().subtract(calculateCommission()).subtract(calculateTax()); diff --git a/src/main/java/Model/Share.java b/src/main/java/Model/Share.java index d7fee43..05a4c08 100644 --- a/src/main/java/Model/Share.java +++ b/src/main/java/Model/Share.java @@ -3,7 +3,9 @@ import java.math.BigDecimal; /** - * Share class. + * Represents a share holding of a stock. + * A Share encapsulates a quantity of a stock purchased at a specific price. + * It provides information about the stock and the purchase terms. */ public class Share { @@ -12,11 +14,12 @@ public class Share { private final BigDecimal purchasePrice; /** - * Share method that has stock, quantity and purchaseprice. + * Constructs a new Share with the specified stock, quantity, and purchase price. * - * @param stock stock name - * @param quantity quantity of stock - * @param purchasePrice price of stock + * @param stock the Stock being held. Cannot be null + * @param quantity the number of shares held. Must be greater than zero + * @param purchasePrice the price per share at purchase. Cannot be null or negative + * @throws IllegalArgumentException if any parameter is invalid */ public Share(Stock stock, BigDecimal quantity, BigDecimal purchasePrice) { if (stock == null) { @@ -40,14 +43,29 @@ public Share(Stock stock, BigDecimal quantity, BigDecimal purchasePrice) { this.purchasePrice = purchasePrice; } + /** + * Returns the Stock associated with this share. + * + * @return the stock being held + */ public Stock getStock() { return stock; } + /** + * Returns the quantity of shares held. + * + * @return the number of shares + */ public BigDecimal getQuantity() { return quantity; } + /** + * Returns the price per share at the time of purchase. + * + * @return the purchase price + */ public BigDecimal getPurchasePrice() { return purchasePrice; } diff --git a/src/main/java/Model/Stock.java b/src/main/java/Model/Stock.java index ca117b5..3c6866e 100644 --- a/src/main/java/Model/Stock.java +++ b/src/main/java/Model/Stock.java @@ -5,7 +5,10 @@ import java.util.List; /** - * STock class. + * Represents a stock traded on the exchange. + * A Stock tracks the company's trading symbol, company name, and historical price + * data. Prices are maintained in chronological order, with the latest price being + * the current sales price. */ public class Stock { @@ -14,7 +17,12 @@ public class Stock { private final List prices; /** - * Stock method that includes symbol of the stock, company and the salesprice. + * Constructs a new Stock with the given symbol, company name, and initial sales price. + * + * @param symbol the unique ticker symbol (e.g., "AAPL"). Cannot be null or blank + * @param company the company name. Cannot be null or blank + * @param salesPrice the initial sales price. Cannot be null or negative + * @throws IllegalArgumentException if any parameter is invalid */ public Stock(String symbol, String company, BigDecimal salesPrice) { if (symbol == null || symbol.isBlank()) { @@ -36,20 +44,38 @@ public Stock(String symbol, String company, BigDecimal salesPrice) { this.prices.add(salesPrice); } + /** + * Returns the ticker symbol of this stock. + * + * @return the stock symbol + */ public String getSymbol() { return symbol; } + /** + * Returns the company name associated with this stock. + * + * @return the company name + */ public String getCompany() { return company; } + /** + * Returns the current (most recent) sales price of this stock. + * + * @return the current sales price + */ public BigDecimal getSalesPrice() { return prices.get(prices.size() - 1); } - + /** - * Method that makes new salesprice. + * Records a new sales price for this stock. + * + * @param price the new sales price. Cannot be null or negative + * @throws IllegalArgumentException if price is invalid */ public void addNewSalesPrice(BigDecimal price) { if (price == null) { @@ -62,7 +88,7 @@ public void addNewSalesPrice(BigDecimal price) { } public List getHistoricalPrices() { - return new ArrayList<>(prices); // returnerer en kopi for å beskytte selve listen + return new ArrayList<>(prices); // returns a copy to protect the list itself } public BigDecimal getHighestPrice() { diff --git a/src/main/java/Model/Transaction.java b/src/main/java/Model/Transaction.java index cedd4d2..a8eb4f1 100644 --- a/src/main/java/Model/Transaction.java +++ b/src/main/java/Model/Transaction.java @@ -1,7 +1,10 @@ package Model; /** - * Transaction class. + * Abstract base class for all transactions (purchases and sales). + * A Transaction represents a trading action taken by a player on a specific week. + * Transactions use the Strategy pattern via TransactionCalculator to compute costs + * and can be committed to apply their effects to the player's portfolio. */ public abstract class Transaction { private Share share; @@ -9,6 +12,14 @@ public abstract class Transaction { private TransactionCalculator calculator; protected boolean committed; + /** + * Constructs a Transaction for the specified share in the given week. + * + * @param share the share being traded. Cannot be null + * @param week the week number when the transaction occurs. Must be at least 1 + * @param calculator the calculator for transaction costs. Cannot be null + * @throws IllegalArgumentException if any parameter is invalid + */ protected Transaction(Share share, int week, TransactionCalculator calculator) { if (share == null) { throw new IllegalArgumentException("Share cannot be null"); @@ -25,22 +36,49 @@ protected Transaction(Share share, int week, TransactionCalculator calculator) { this.calculator = calculator; } + /** + * Returns the share being traded in this transaction. + * + * @return the share + */ public Share getShare() { return this.share; } + /** + * Returns the week number when this transaction occurs. + * + * @return the week number + */ public int getWeek() { return this.week; } + /** + * Returns the calculator used to compute transaction costs. + * + * @return the transaction calculator + */ public TransactionCalculator getCalculator() { return this.calculator; } + /** + * Returns whether this transaction has been committed. + * + * @return true if committed; false otherwise + */ public boolean isCommitted() { return this.committed; } + /** + * Commits this transaction, applying its effects to the player's portfolio. + * + * @param player the player executing the transaction. Cannot be null + * @throws IllegalArgumentException if player is null + * @throws IllegalStateException if the transaction has already been committed + */ public abstract void commit(Player player); } diff --git a/src/main/java/Model/TransactionArchive.java b/src/main/java/Model/TransactionArchive.java index db7a50c..4af5bff 100644 --- a/src/main/java/Model/TransactionArchive.java +++ b/src/main/java/Model/TransactionArchive.java @@ -5,37 +5,60 @@ import java.util.stream.Collectors; /** - * TransactionArchive class. + * Maintains a chronological archive of all transactions executed by a player. + * The TransactionArchive allows retrieval of transactions by week and type + * (purchase or sale), and provides statistics about trading activity. */ public class TransactionArchive { - private List transactions; + private final List transactions; + /** + * Constructs a new empty TransactionArchive. + */ public TransactionArchive() { this.transactions = new ArrayList<>(); } + /** + * Adds a transaction to the archive. + * + * @param transaction the transaction to add. Cannot be null + * @return true if the transaction was added successfully + * @throws IllegalArgumentException if transaction is null + */ public boolean add(Transaction transaction) { if (transaction == null) { - throw new IllegalArgumentException("Should not be null"); // Eller NullPointerExeption? + throw new IllegalArgumentException("Should not be null"); } return transactions.add(transaction); } + /** + * Checks if the archive is empty. + * + * @return true if no transactions have been recorded + */ public boolean isEmpty() { return transactions.isEmpty(); } + /** + * Returns all transactions that occurred in a specific week. + * + * @param week the week number + * @return a list of transactions in that week + */ public List getTransactions(int week) { return transactions.stream() .filter(transaction -> transaction.getWeek() == week).collect(Collectors.toList()); } /** - * List of all purchases in one week. + * Returns all purchases that occurred in a specific week. * - * @param week the week to find purchases for - * @return returns the purchases + * @param week the week number + * @return a list of purchases in that week */ public List getPurchase(int week) { return transactions.stream() @@ -45,10 +68,10 @@ public List getPurchase(int week) { } /** - * Gets sales for that week. + * Returns all sales that occurred in a specific week. * - * @param week the week to find sales for - * @return returns the sales + * @param week the week number + * @return a list of sales in that week */ public List getSale(int week) { return transactions.stream() @@ -57,10 +80,20 @@ public List getSale(int week) { .map(transaction -> (Sale) transaction).collect(Collectors.toList()); } + /** + * Returns a defensive copy of all transactions in the archive. + * + * @return a copy of the transaction list + */ public List getAllTransactions() { return new ArrayList<>(transactions); } + /** + * Counts the number of distinct weeks with trading activity. + * + * @return the number of unique weeks represented in the archive + */ public int countDistinctWeeks() { return (int) transactions.stream().map(Transaction::getWeek).distinct().count(); } diff --git a/src/main/java/Model/TransactionCalculator.java b/src/main/java/Model/TransactionCalculator.java index 73fe750..2691e02 100644 --- a/src/main/java/Model/TransactionCalculator.java +++ b/src/main/java/Model/TransactionCalculator.java @@ -3,16 +3,37 @@ import java.math.BigDecimal; /** - * TransactionCalculator class. + * Strategy interface for calculating transaction costs and proceeds. + * Implementations compute fees and totals for different transaction types + * (purchases and sales), following the Strategy design pattern. */ public interface TransactionCalculator { - // Methods in interface is automatically public abstract. + /** + * Calculates the base cost/revenue without fees. + * + * @return the gross amount + */ BigDecimal calculateGross(); + /** + * Calculates the commission fee on the transaction. + * + * @return the commission amount + */ BigDecimal calculateCommission(); + /** + * Calculates any applicable taxes on the transaction. + * + * @return the tax amount + */ BigDecimal calculateTax(); + /** + * Calculates the final total after all fees and taxes. + * + * @return the total transaction amount + */ BigDecimal calculateTotal(); } diff --git a/src/main/java/Model/TransactionFactory.java b/src/main/java/Model/TransactionFactory.java index 691d41b..1f799d1 100644 --- a/src/main/java/Model/TransactionFactory.java +++ b/src/main/java/Model/TransactionFactory.java @@ -2,18 +2,29 @@ /** * Factory for creating transaction objects. + * Uses the Factory pattern to encapsulate transaction creation logic, + * allowing clients to create Purchase and Sale transactions without + * knowing implementation details. */ public class TransactionFactory { /** - * Create purchase transaction. + * Creates a new Purchase transaction. + * + * @param share the share to purchase. Cannot be null + * @param week the week when the purchase occurs. Must be at least 1 + * @return a new Purchase transaction */ public static Transaction createPurchase(Share share, int week) { return new Purchase(share, week); } /** - * Create sale transaction. + * Creates a new Sale transaction. + * + * @param share the share to sell. Cannot be null + * @param week the week when the sale occurs. Must be at least 1 + * @return a new Sale transaction */ public static Transaction createSale(Share share, int week) { return new Sale(share, week); diff --git a/src/main/java/View/GameSetupScene.java b/src/main/java/View/GameSetupScene.java index 1e2d66d..44052ca 100644 --- a/src/main/java/View/GameSetupScene.java +++ b/src/main/java/View/GameSetupScene.java @@ -18,14 +18,13 @@ /** * Displays the game setup scene where players configure their game parameters. - * * Allows players to enter their name, starting capital, and select a stock data file * before starting the main game. Notifies listeners when the game starts. */ public class GameSetupScene { - private Scene scene; - private Consumer onGameStart; + private final Scene scene; + private final Consumer onGameStart; private File selectedFile; private Label fileLabel; @@ -113,7 +112,7 @@ private Scene createScene() { Region buttonSpacer = new Region(); buttonSpacer.setMinHeight(10); - // Button row — buttons fill full card width equally + // Button row HBox buttonBox = new HBox(12); buttonBox.setAlignment(Pos.CENTER_LEFT); buttonBox.setMaxWidth(Double.MAX_VALUE); @@ -142,7 +141,7 @@ private Scene createScene() { buttonBox ); - // Center the card without stretching it vertically + // Centers the root VBox root = new VBox(card); root.setAlignment(Pos.CENTER); root.setFillWidth(false); @@ -250,7 +249,6 @@ public Scene getScene() { /** * Data class encapsulating game setup parameters. - * * Contains the exchange configured with stocks, player name, and starting capital. */ public static class StartGameData { diff --git a/src/main/java/View/Launcher.java b/src/main/java/View/Launcher.java index b56a4bf..fb7bc49 100644 --- a/src/main/java/View/Launcher.java +++ b/src/main/java/View/Launcher.java @@ -1,9 +1,16 @@ package View; /** - * Launcher class for the Stock Trading Game. + * Entry point for launching the Stock Trading Game application. + * This launcher delegates to the main StockTradingGameApp class to start the JavaFX + * application. It provides a conventional main method entry point. */ public class Launcher { + /** + * Main method to launch the application. + * + * @param args command-line arguments (not used) + */ public static void main(String[] args) { StockTradingGameApp.main(args); } diff --git a/src/main/java/View/MainGameScene.java b/src/main/java/View/MainGameScene.java index fc7bd81..6f4cd27 100644 --- a/src/main/java/View/MainGameScene.java +++ b/src/main/java/View/MainGameScene.java @@ -1,585 +1,693 @@ package View; -import javafx.geometry.Insets; +import Model.*; +import java.math.BigDecimal; +import java.util.List; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; import javafx.geometry.Pos; import javafx.scene.Scene; import javafx.scene.control.*; import javafx.scene.layout.*; -import javafx.collections.FXCollections; -import javafx.collections.ObservableList; -import Model.*; -import java.math.BigDecimal; -import java.util.List; /** - * Main game UI. Implements ExchangeObserver so the view automatically refreshes - * whenever the Exchange notifies it of a state change (week advance or trade). + * The main game user interface scene. + * Displays the game board with stocks, portfolio, trading interface, and transaction + * history. Implements ExchangeObserver to automatically refresh UI when the game state changes. */ public class MainGameScene implements ExchangeObserver { - private Scene scene; - private Exchange exchange; - private Player player; - private Runnable onExit; - private Label statusLabel; - - // References to UI components that need refreshing - private TableView portfolioTable; - private ListView holdingsList; - private TableView historyTable; - private ComboBox weekFilterCombo; - - public MainGameScene(Exchange exchange, Player player, Runnable onExit) { - this.exchange = exchange; - this.player = player; - this.onExit = onExit; - - // Register this view as an observer of the exchange - this.exchange.addObserver(this); - - this.scene = createScene(); - } - - /** - * Called automatically by Exchange whenever its state changes. - * Refreshes all UI elements to reflect the latest data. - */ - @Override - public void onExchangeUpdated(Exchange exchange) { - updateStatus(); - refreshAllUI(); - } - - private Scene createScene() { - VBox root = new VBox(0); - - // TOP STATUS BAR - HBox topBar = new HBox(12); - topBar.getStyleClass().add("top-bar"); - topBar.setAlignment(Pos.CENTER_LEFT); - - statusLabel = new Label(); - statusLabel.getStyleClass().add("status-label"); - updateStatus(); - - Separator sep = new Separator(javafx.geometry.Orientation.VERTICAL); - sep.setPrefHeight(20); - - Button nextWeekBtn = new Button("Next week"); - nextWeekBtn.getStyleClass().addAll("action-button"); - nextWeekBtn.setOnAction(e -> exchange.advance()); // observer handles the UI update - - Button exitBtn = new Button("Exit"); - exitBtn.getStyleClass().add("exit-button"); - exitBtn.setOnAction(e -> { - if (confirm("Exit Game?", "Final Net Worth: $" + formatMoney(getNetWorth()))) { - exchange.removeObserver(this); // clean up before closing - onExit.run(); + private final Scene scene; + private final Exchange exchange; + private final Player player; + private final Runnable onExit; + private Label statusLabel; + + // References to UI components that need refreshing + private TableView portfolioTable; + private ListView holdingsList; + private TableView historyTable; + private ComboBox weekFilterCombo; + + /** + * Constructs the main game scene. + * + * @param exchange the Exchange managing the game state. Cannot be null + * @param player the Player being displayed. Cannot be null + * @param onExit callback to run when exiting the game. Cannot be null + */ + public MainGameScene(Exchange exchange, Player player, Runnable onExit) { + this.exchange = exchange; + this.player = player; + this.onExit = onExit; + + // Register this view as an observer of the exchange + this.exchange.addObserver(this); + + this.scene = createScene(); + } + + /** + * Callback invoked when the Exchange notifies observers of state changes. + * + * @param exchange the Exchange that changed + */ + @Override + public void onExchangeUpdated(Exchange exchange) { + updateStatus(); + refreshAllUI(); + } + + /** + * Creates the main game scene layout. + * + * @return the constructed Scene + */ + private Scene createScene() { + VBox root = new VBox(0); + + // TOP STATUS BAR + HBox topBar = new HBox(12); + topBar.getStyleClass().add("top-bar"); + topBar.setAlignment(Pos.CENTER_LEFT); + + statusLabel = new Label(); + statusLabel.getStyleClass().add("status-label"); + updateStatus(); + + Separator sep = new Separator(javafx.geometry.Orientation.VERTICAL); + sep.setPrefHeight(20); + + Button nextWeekBtn = new Button("Next week"); + nextWeekBtn.getStyleClass().addAll("action-button"); + nextWeekBtn.setOnAction(e -> exchange.advance()); // observer handles the UI update + + Button exitBtn = new Button("Exit"); + exitBtn.getStyleClass().add("exit-button"); + exitBtn.setOnAction(e -> { + if (confirm("Exit Game?", "Final Net Worth: $" + formatMoney(getNetWorth()))) { + exchange.removeObserver(this); // clean up before closing + onExit.run(); + } + }); + + topBar.getChildren().addAll(statusLabel, sep, nextWeekBtn, exitBtn); + + // MAIN CONTENT TABS + TabPane tabs = new TabPane(); + tabs.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE); + tabs.getTabs().addAll( + new Tab("Stocks", createStocksPanel()), + new Tab("Portfolio", createPortfolioPanel()), + new Tab("Trade", createTradePanel()), + new Tab("History", createHistoryPanel()) + ); + + VBox.setVgrow(tabs, Priority.ALWAYS); + root.getChildren().addAll(topBar, tabs); + + Scene scene = new Scene(root, 1000, 700); + scene.getStylesheets().add( + getClass().getResource("/Style/Global.css").toExternalForm() + ); + return scene; + } + + /** + * Creates the stocks panel displaying available stocks. + * + * @return the stocks panel + */ + private VBox createStocksPanel() { + VBox panel = new VBox(10); + panel.getStyleClass().add("content-area"); + + HBox searchBox = new HBox(8); + searchBox.setAlignment(Pos.CENTER_LEFT); + + TextField search = new TextField(); + search.setPromptText("Search symbol or company..."); + HBox.setHgrow(search, Priority.ALWAYS); + + Button searchBtn = new Button("Search"); + searchBtn.getStyleClass().add("action-button"); + + ComboBox filter = new ComboBox<>(); + filter.setItems(FXCollections.observableArrayList("All", "Gainers", "Losers")); + filter.setValue("All"); + + TableView table = new TableView<>(); + addStockColumns(table); + + Runnable loadStocks = () -> { + List stocks; + String filterVal = filter.getValue(); + String searchVal = search.getText().trim(); + + if (null == filterVal) { + stocks = exchange.findStocks(searchVal); + } else { + stocks = switch (filterVal) { + case "Gainers" -> exchange.getGainers(20); + case "Losers" -> exchange.getLosers(20); + default -> exchange.findStocks(searchVal); + }; + } + + ObservableList data = FXCollections.observableArrayList(); + for (Stock s : stocks) { + data.add(new StockRow(s)); + } + table.setItems(data); + }; + + searchBtn.setOnAction(e -> loadStocks.run()); + filter.setOnAction(e -> loadStocks.run()); + search.setOnAction(e -> loadStocks.run()); + + searchBox.getChildren().addAll(search, searchBtn, filter); + loadStocks.run(); + + VBox.setVgrow(table, Priority.ALWAYS); + panel.getChildren().addAll(searchBox, table); + return panel; + } + + /** + * Creates the portfolio panel showing held shares and net worth. + * + * @return the portfolio panel + */ + private VBox createPortfolioPanel() { + VBox panel = new VBox(10); + panel.getStyleClass().add("content-area"); + + Label heading = new Label("Holdings:"); + + portfolioTable = new TableView<>(); + addPortfolioColumns(portfolioTable); + updatePortfolio(portfolioTable); + + Button refresh = new Button("Refresh"); + refresh.getStyleClass().add("action-button"); + refresh.setOnAction(e -> updatePortfolio(portfolioTable)); + + Button sellSelected = new Button("Sell Selected"); + sellSelected.getStyleClass().add("action-button"); + sellSelected.setOnAction(e -> { + PortfolioRow selected = portfolioTable.getSelectionModel().getSelectedItem(); + if (selected == null) { + alert("Error", "Select a holding to sell."); + return; + } + Transaction trans = exchange.sell(selected.s, player); // observer fires refresh + if (trans != null && trans.isCommitted()) { + showConfirmation("Sale successful", trans); + } else { + alert("Failed", "Could not complete the sale."); + } + }); + + HBox buttons = new HBox(10, refresh, sellSelected); + buttons.setAlignment(Pos.CENTER_LEFT); + + VBox.setVgrow(portfolioTable, Priority.ALWAYS); + panel.getChildren().addAll(heading, portfolioTable, buttons); + return panel; + } + + /** + * Creates the trading panel with buy and sell tabs. + * + * @return the trading panel + */ + private VBox createTradePanel() { + VBox panel = new VBox(0); + panel.getStyleClass().add("content-area"); + + TabPane tradeTabs = new TabPane(); + tradeTabs.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE); + tradeTabs.getTabs().addAll( + new Tab("Buy", createBuyTab()), + new Tab("Sell", createSellTab()) + ); + + VBox.setVgrow(tradeTabs, Priority.ALWAYS); + panel.getChildren().add(tradeTabs); + return panel; + } + + /** + * Creates the buy transaction tab. + * + * @return the buy tab content + */ + private VBox createBuyTab() { + VBox box = new VBox(10); + box.getStyleClass().add("content-area"); + + Label heading = new Label("Buy Stocks:"); + + GridPane form = new GridPane(); + form.setHgap(10); + form.setVgap(10); + + TextField stockField = new TextField(); + stockField.setPromptText("Stock symbol (e.g. AAPL)"); + + TextField qtyField = new TextField(); + qtyField.setPromptText("Quantity"); + + form.add(new Label("Symbol:"), 0, 0); + form.add(stockField, 1, 0); + form.add(new Label("Quantity:"), 0, 1); + form.add(qtyField, 1, 1); + + Label infoLabel = new Label(); + + stockField.textProperty() + .addListener((obs, old, val) -> updateBuyInfo(val, qtyField, infoLabel)); + qtyField.textProperty() + .addListener((obs, old, val) -> updateBuyInfo(stockField.getText(), qtyField, infoLabel)); + + Button buyBtn = new Button("Buy"); + buyBtn.getStyleClass().add("action-button"); + buyBtn.setOnAction(e -> { + try { + Stock s = exchange.getStock(stockField.getText().toUpperCase()); + if (s == null) { + alert("Error", "Stock not found"); + return; + } + BigDecimal qty = new BigDecimal(qtyField.getText()); + Transaction trans = exchange.buy(s.getSymbol(), qty, player); // observer fires refresh + if (trans != null && trans.isCommitted()) { + showConfirmation("Purchase successful", trans); + stockField.clear(); + qtyField.clear(); + infoLabel.setText(""); + } else { + alert("Failed", "Insufficient funds or error"); + } + } catch (Exception ex) { + alert("Error", ex.getMessage()); + } + }); + + box.getChildren().addAll(heading, form, infoLabel, buyBtn); + return box; + } + + /** + * Creates the sell transaction tab. + * + * @return the sell tab content + */ + private VBox createSellTab() { + VBox box = new VBox(10); + box.getStyleClass().add("content-area"); + + Label heading = new Label("Your Holdings:"); + + holdingsList = new ListView<>(); + holdingsList.setPrefHeight(400); + + holdingsList.setCellFactory(lv -> new ListCell() { + @Override + protected void updateItem(Share s, boolean empty) { + super.updateItem(s, empty); + if (empty || s == null) { + setText(null); + } else { + setText( + s.getStock().getSymbol() + " - " + + s.getQuantity() + " @ $" + + formatMoney(s.getStock().getSalesPrice()) + ); } - }); - - topBar.getChildren().addAll(statusLabel, sep, nextWeekBtn, exitBtn); - - // MAIN CONTENT TABS - TabPane tabs = new TabPane(); - tabs.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE); - tabs.getTabs().addAll( - new Tab("Stocks", createStocksPanel()), - new Tab("Portfolio", createPortfolioPanel()), - new Tab("Trade", createTradePanel()), - new Tab("History", createHistoryPanel()) - ); - - VBox.setVgrow(tabs, Priority.ALWAYS); - root.getChildren().addAll(topBar, tabs); - - Scene scene = new Scene(root, 1000, 700); - scene.getStylesheets().add( - getClass().getResource("/Style/Global.css").toExternalForm() + } + }); + + updateHoldingsList(holdingsList); + + // Numer of rows + HBox quantityRow = new HBox(8); + quantityRow.setAlignment(Pos.CENTER_LEFT); + TextField quantityField = new TextField(); + quantityField.setPromptText("Antall"); + quantityField.setPrefWidth(120); + quantityRow.getChildren().addAll(new Label("Quantity to sell:"), quantityField); + + // Fill in the maximum number automatically when selecting a holding + holdingsList.getSelectionModel().selectedItemProperty().addListener((obs, old, selected) -> { + if (selected != null) { + quantityField.setText(selected.getQuantity().toPlainString()); + } + }); + + Button sellBtn = new Button("Sell Selected"); + sellBtn.setOnAction(e -> { + Share selected = holdingsList.getSelectionModel().getSelectedItem(); + if (selected == null) { + alert("Error", "Select a holding to sell"); + return; + } + + try { + BigDecimal quantity = new BigDecimal(quantityField.getText().trim()); + + if (quantity.compareTo(BigDecimal.ZERO) <= 0) { + alert("Error", "Quantity must be greater than zero"); + return; + } + + Transaction trans = exchange.sell(selected, quantity, player); // observer fires refresh + + if (trans != null && trans.isCommitted()) { + showConfirmation("Sale successful", trans); + } else { + alert("Failed", "Could not complete sale. Check quantity."); + } + } catch (NumberFormatException ex) { + alert("Error", "Enter a valid number for quantity"); + } + }); + + box.getChildren().addAll(heading, holdingsList, quantityRow, sellBtn); + + return box; + } + + /** + * Creates the transaction history panel. + * + * @return the history panel + */ + private VBox createHistoryPanel() { + VBox panel = new VBox(10); + panel.getStyleClass().add("content-area"); + + weekFilterCombo = new ComboBox<>(); + updateWeekCombo(weekFilterCombo); + + historyTable = new TableView<>(); + addHistoryColumns(historyTable); + updateHistory(historyTable, null); + + weekFilterCombo.setOnAction(e -> updateHistory( + historyTable, + weekFilterCombo.getValue() == null || weekFilterCombo.getValue() == 0 + ? null : weekFilterCombo.getValue() + )); + + HBox filterRow = new HBox(8); + filterRow.setAlignment(Pos.CENTER_LEFT); + filterRow.getChildren().addAll(new Label("Week:"), weekFilterCombo); + + VBox.setVgrow(historyTable, Priority.ALWAYS); + panel.getChildren().addAll(filterRow, historyTable); + return panel; + } + + // ---- Column helpers ---- + + @SuppressWarnings("unchecked") +private void addStockColumns(TableView table) { + table.getColumns().addAll( + createCol("Symbol", 90, "symbol"), + createCol("Company", 180, "company"), + createCol("Price", 90, "price"), + createCol("Change", 90, "change"), + createCol("High", 90, "high"), + createCol("Low", 90, "low") + ); + } + + @SuppressWarnings("unchecked") + private void addPortfolioColumns(TableView table) { + table.getColumns().addAll( + createCol("Symbol", 90, "symbol"), + createCol("Qty", 70, "qty"), + createCol("Buy Price", 100, "buyPrice"), + createCol("Current Price", 120, "currentPrice"), + createCol("Gain/Loss", 100, "gainLoss") + ); + } + + @SuppressWarnings("unchecked") + private void addHistoryColumns(TableView table) { + table.getColumns().addAll( + createCol("Week", 70, "week"), + createCol("Type", 70, "type"), + createCol("Symbol", 90, "symbol"), + createCol("Qty", 70, "qty"), + createCol("Total", 110, "total") + ); + } + + private TableColumn createCol(String header, int width, String property) { + TableColumn col = new TableColumn<>(header); + col.setPrefWidth(width); + col.setCellValueFactory(cellData -> { + try { + return new javafx.beans.property.SimpleStringProperty( + cellData.getValue().getClass() + .getMethod("get" + capitalize(property)) + .invoke(cellData.getValue()).toString() ); - return scene; + } catch (Exception e) { + return new javafx.beans.property.SimpleStringProperty(""); + } + }); + return col; + } + + private String capitalize(String s) { + return s.substring(0, 1).toUpperCase() + s.substring(1); + } + + // ---- Data updaters ---- + + private void updatePortfolio(TableView table) { + ObservableList data = FXCollections.observableArrayList(); + for (Share s : player.getPortfolio().getShares()) { + data.add(new PortfolioRow(s)); } - - private VBox createStocksPanel() { - VBox panel = new VBox(10); - panel.getStyleClass().add("content-area"); - - HBox searchBox = new HBox(8); - searchBox.setAlignment(Pos.CENTER_LEFT); - - TextField search = new TextField(); - search.setPromptText("Search symbol or company..."); - HBox.setHgrow(search, Priority.ALWAYS); - - Button searchBtn = new Button("Search"); - searchBtn.getStyleClass().add("action-button"); - - ComboBox filter = new ComboBox<>(); - filter.setItems(FXCollections.observableArrayList("All", "Gainers", "Losers")); - filter.setValue("All"); - - TableView table = new TableView<>(); - addStockColumns(table); - - Runnable loadStocks = () -> { - List stocks; - String filterVal = filter.getValue(); - String searchVal = search.getText().trim(); - - if ("Gainers".equals(filterVal)) { - stocks = exchange.getGainers(20); - } else if ("Losers".equals(filterVal)) { - stocks = exchange.getLosers(20); - } else { - stocks = exchange.findStocks(searchVal); - } - - ObservableList data = FXCollections.observableArrayList(); - for (Stock s : stocks) { - data.add(new StockRow(s)); - } - table.setItems(data); - }; - - searchBtn.setOnAction(e -> loadStocks.run()); - filter.setOnAction(e -> loadStocks.run()); - search.setOnAction(e -> loadStocks.run()); - - searchBox.getChildren().addAll(search, searchBtn, filter); - loadStocks.run(); - - VBox.setVgrow(table, Priority.ALWAYS); - panel.getChildren().addAll(searchBox, table); - return panel; + table.setItems(data); + } + + private void updateHoldingsList(ListView list) { + ObservableList items = FXCollections.observableArrayList(); + items.addAll(player.getPortfolio().getShares()); + list.setItems(items); + } + + private void updateHistory(TableView table, Integer week) { + ObservableList data = FXCollections.observableArrayList(); + List trans = week == null + ? player.getTransactionArchive().getAllTransactions() + : player.getTransactionArchive().getTransactions(week); + for (Transaction t : trans) { + data.add(new HistoryRow(t)); } - - private VBox createPortfolioPanel() { - VBox panel = new VBox(10); - panel.getStyleClass().add("content-area"); - - Label heading = new Label("Holdings:"); - - portfolioTable = new TableView<>(); - addPortfolioColumns(portfolioTable); - updatePortfolio(portfolioTable); - - Button refresh = new Button("Refresh"); - refresh.getStyleClass().add("action-button"); - refresh.setOnAction(e -> updatePortfolio(portfolioTable)); - - Button sellSelected = new Button("Sell Selected"); - sellSelected.getStyleClass().add("action-button"); - sellSelected.setOnAction(e -> { - PortfolioRow selected = portfolioTable.getSelectionModel().getSelectedItem(); - if (selected == null) { - alert("Error", "Select a holding to sell."); - return; - } - Transaction trans = exchange.sell(selected.s, player); // observer fires refresh - if (trans != null && trans.isCommitted()) { - showConfirmation("Sale successful", trans); - } else { - alert("Failed", "Could not complete the sale."); - } - }); - - HBox buttons = new HBox(10, refresh, sellSelected); - buttons.setAlignment(Pos.CENTER_LEFT); - - VBox.setVgrow(portfolioTable, Priority.ALWAYS); - panel.getChildren().addAll(heading, portfolioTable, buttons); - return panel; + table.setItems(data); + } + + private void updateWeekCombo(ComboBox combo) { + ObservableList weeks = FXCollections.observableArrayList(); + weeks.add(0); + int total = player.getTransactionArchive().countDistinctWeeks(); + for (int i = 1; i <= total; i++) { + weeks.add(i); } - - private VBox createTradePanel() { - VBox panel = new VBox(0); - panel.getStyleClass().add("content-area"); - - TabPane tradeTabs = new TabPane(); - tradeTabs.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE); - tradeTabs.getTabs().addAll( - new Tab("Buy", createBuyTab()), - new Tab("Sell", createSellTab()) - ); - - VBox.setVgrow(tradeTabs, Priority.ALWAYS); - panel.getChildren().add(tradeTabs); - return panel; + combo.setItems(weeks); + combo.setValue(0); + } + + private void updateBuyInfo(String symbol, TextField qtyField, Label label) { + try { + if (symbol == null || symbol.isEmpty()) { + return; + } + Stock s = exchange.getStock(symbol.toUpperCase()); + if (s == null) { + return; + } + BigDecimal qty = new BigDecimal(qtyField.getText()); + BigDecimal gross = s.getSalesPrice().multiply(qty); + BigDecimal comm = gross.multiply(new BigDecimal("0.005")); + label.setText("Gross: $" + formatMoney(gross) + + " | Commission: $" + formatMoney(comm) + + " | Total: $" + formatMoney(gross.add(comm))); + } catch (Exception e) { + // ignore partial input } - - private VBox createBuyTab() { - VBox box = new VBox(10); - box.getStyleClass().add("content-area"); - - Label heading = new Label("Buy Stocks:"); - - GridPane form = new GridPane(); - form.setHgap(10); - form.setVgap(10); - - TextField stockField = new TextField(); - stockField.setPromptText("Stock symbol (e.g. AAPL)"); - - TextField qtyField = new TextField(); - qtyField.setPromptText("Quantity"); - - form.add(new Label("Symbol:"), 0, 0); - form.add(stockField, 1, 0); - form.add(new Label("Quantity:"), 0, 1); - form.add(qtyField, 1, 1); - - Label infoLabel = new Label(); - - stockField.textProperty().addListener((obs, old, val) -> updateBuyInfo(val, qtyField, infoLabel)); - qtyField.textProperty().addListener((obs, old, val) -> updateBuyInfo(stockField.getText(), qtyField, infoLabel)); - - Button buyBtn = new Button("Buy"); - buyBtn.getStyleClass().add("action-button"); - buyBtn.setOnAction(e -> { - try { - Stock s = exchange.getStock(stockField.getText().toUpperCase()); - if (s == null) { - alert("Error", "Stock not found"); - return; - } - BigDecimal qty = new BigDecimal(qtyField.getText()); - Transaction trans = exchange.buy(s.getSymbol(), qty, player); // observer fires refresh - if (trans != null && trans.isCommitted()) { - showConfirmation("Purchase successful", trans); - stockField.clear(); - qtyField.clear(); - infoLabel.setText(""); - } else { - alert("Failed", "Insufficient funds or error"); - } - } catch (Exception ex) { - alert("Error", ex.getMessage()); - } - }); - - box.getChildren().addAll(heading, form, infoLabel, buyBtn); - return box; + } + + private void updateStatus() { + PlayerStatus playerStatus = player.getStatus(); + statusLabel.setText( + "Player: " + player.getName() + + " | Status: " + playerStatus.getDisplayName() + + " | Week: " + exchange.getWeek() + + " | Cash: $" + formatMoney(player.getMoney()) + + " | Net Worth: $" + formatMoney(getNetWorth()) + ); + } + + private void refreshAllUI() { + if (portfolioTable != null) { + updatePortfolio(portfolioTable); } - - private VBox createSellTab() { - VBox box = new VBox(10); - box.getStyleClass().add("content-area"); - - Label heading = new Label("Your Holdings:"); - - holdingsList = new ListView<>(); - holdingsList.setPrefHeight(400); - - holdingsList.setCellFactory(lv -> new ListCell() { - @Override - protected void updateItem(Share s, boolean empty) { - super.updateItem(s, empty); - if (empty || s == null) { - setText(null); - } else { - setText( - s.getStock().getSymbol() + " - " + - s.getQuantity() + " @ $" + - formatMoney(s.getStock().getSalesPrice()) - ); - } - } - }); - - updateHoldingsList(holdingsList); - - // Antall Rad - HBox quantityRow = new HBox(8); - quantityRow.setAlignment(Pos.CENTER_LEFT); - TextField quantityField = new TextField(); - quantityField.setPromptText("Antall"); - quantityField.setPrefWidth(120); - quantityRow.getChildren().addAll(new Label("Quantity to sell:"), quantityField); - - // Fyll inn maks anntall automatisk når man velger en holding - holdingsList.getSelectionModel().selectedItemProperty().addListener((obs, old, selected) -> { - if (selected != null) { - quantityField.setText(selected.getQuantity().toPlainString()); - } - }); - - Button sellBtn = new Button("Sell Selected"); - sellBtn.setOnAction(e -> { - Share selected = holdingsList.getSelectionModel().getSelectedItem(); - if (selected == null) { - alert("Error", "Select a holding to sell"); - return; - } - - try { - BigDecimal quantity = new BigDecimal(quantityField.getText().trim()); - - if (quantity.compareTo(BigDecimal.ZERO) <= 0) { - alert("Error", "Quantity must be greater than zero"); - return; - } - - Transaction trans = exchange.sell(selected, quantity, player); // observer fires refresh - - if (trans != null && trans.isCommitted()) { - showConfirmation("Sale successful", trans); - } - - else { - alert("Failed", "Could not complete sale. Check quantity."); - } - } - - catch (NumberFormatException ex) { - alert("Error", "Enter a valid number for quantity"); - } - }); - - box.getChildren().addAll(heading, holdingsList, quantityRow, sellBtn); - - return box; + if (holdingsList != null) { + updateHoldingsList(holdingsList); } - - private VBox createHistoryPanel() { - VBox panel = new VBox(10); - panel.getStyleClass().add("content-area"); - - weekFilterCombo = new ComboBox<>(); + if (historyTable != null) { + updateHistory(historyTable, + weekFilterCombo != null && weekFilterCombo.getValue() != null + ? weekFilterCombo.getValue() : null); + if (weekFilterCombo != null) { updateWeekCombo(weekFilterCombo); - - historyTable = new TableView<>(); - addHistoryColumns(historyTable); - updateHistory(historyTable, null); - - weekFilterCombo.setOnAction(e -> updateHistory( - historyTable, - weekFilterCombo.getValue() == null || weekFilterCombo.getValue() == 0 - ? null : weekFilterCombo.getValue() - )); - - HBox filterRow = new HBox(8); - filterRow.setAlignment(Pos.CENTER_LEFT); - filterRow.getChildren().addAll(new Label("Week:"), weekFilterCombo); - - VBox.setVgrow(historyTable, Priority.ALWAYS); - panel.getChildren().addAll(filterRow, historyTable); - return panel; + } } - - // ---- Column helpers ---- - - private void addStockColumns(TableView table) { - table.getColumns().addAll( - createCol("Symbol", 90, "symbol"), - createCol("Company", 180, "company"), - createCol("Price", 90, "price"), - createCol("Change", 90, "change"), - createCol("High", 90, "high"), - createCol("Low", 90, "low") - ); + } + + private BigDecimal getNetWorth() { + return player.getMoney().add(player.getPortfolio().getNetWorth()); + } + + private void showConfirmation(String title, Transaction t) { + StringBuilder msg = new StringBuilder(); + if (t instanceof Purchase) { + Purchase p = (Purchase) t; + msg.append("Bought ").append(formatMoney(p.getShare().getQuantity())) + .append(" @ $").append(formatMoney(p.getShare().getPurchasePrice())) + .append("\nCost: $").append(formatMoney(p.getCalculator().calculateTotal())); + } else { + Sale s = (Sale) t; + msg.append("Sold ").append(formatMoney(s.getShare().getQuantity())) + .append(" @ $").append(formatMoney(s.getShare().getStock().getSalesPrice())) + .append("\nProceeds: $").append(formatMoney(s.getCalculator().calculateTotal())); } - - private void addPortfolioColumns(TableView table) { - table.getColumns().addAll( - createCol("Symbol", 90, "symbol"), - createCol("Qty", 70, "qty"), - createCol("Buy Price", 100, "buyPrice"), - createCol("Current Price", 120, "currentPrice"), - createCol("Gain/Loss", 100, "gainLoss") - ); + alert(title, msg.toString()); + } + + private void alert(String title, String msg) { + Alert a = new Alert(Alert.AlertType.INFORMATION); + a.setTitle(title); + a.setHeaderText(null); + a.setContentText(msg); + a.showAndWait(); + } + + private boolean confirm(String title, String msg) { + Alert a = new Alert(Alert.AlertType.CONFIRMATION); + a.setTitle(title); + a.setHeaderText(null); + a.setContentText(msg); + return a.showAndWait().get() == ButtonType.OK; + } + + private String formatMoney(BigDecimal n) { + return n.setScale(2, java.math.RoundingMode.HALF_UP).toString(); + } + + public Scene getScene() { + return scene; + } + + // ---- Data row classes ---- + + static class StockRow { + Stock s; + + StockRow(Stock s) { + this.s = s; } - private void addHistoryColumns(TableView table) { - table.getColumns().addAll( - createCol("Week", 70, "week"), - createCol("Type", 70, "type"), - createCol("Symbol", 90, "symbol"), - createCol("Qty", 70, "qty"), - createCol("Total", 110, "total") - ); + public String getSymbol() { + return s.getSymbol(); } - private TableColumn createCol(String header, int width, String property) { - TableColumn col = new TableColumn<>(header); - col.setPrefWidth(width); - col.setCellValueFactory(cellData -> { - try { - return new javafx.beans.property.SimpleStringProperty( - cellData.getValue().getClass() - .getMethod("get" + capitalize(property)) - .invoke(cellData.getValue()).toString() - ); - } catch (Exception e) { - return new javafx.beans.property.SimpleStringProperty(""); - } - }); - return col; + public String getCompany() { + return s.getCompany(); } - private String capitalize(String s) { - return s.substring(0, 1).toUpperCase() + s.substring(1); + public String getPrice() { + return "$" + s.getSalesPrice(); } - // ---- Data updaters ---- - - private void updatePortfolio(TableView table) { - ObservableList data = FXCollections.observableArrayList(); - for (Share s : player.getPortfolio().getShares()) { - data.add(new PortfolioRow(s)); - } - table.setItems(data); + public String getChange() { + return "$" + s.getLatestPriceChange(); } - - private void updateHoldingsList(ListView list) { - ObservableList items = FXCollections.observableArrayList(); - items.addAll(player.getPortfolio().getShares()); - list.setItems(items); + + public String getHigh() { + return "$" + s.getHighestPrice(); } - private void updateHistory(TableView table, Integer week) { - ObservableList data = FXCollections.observableArrayList(); - List trans = week == null - ? player.getTransactionArchive().getAllTransactions() - : player.getTransactionArchive().getTransactions(week); - for (Transaction t : trans) { - data.add(new HistoryRow(t)); - } - table.setItems(data); + public String getLow() { + return "$" + s.getLowestPrice(); } - private void updateWeekCombo(ComboBox combo) { - ObservableList weeks = FXCollections.observableArrayList(); - weeks.add(0); - int total = player.getTransactionArchive().countDistinctWeeks(); - for (int i = 1; i <= total; i++) weeks.add(i); - combo.setItems(weeks); - combo.setValue(0); - } + } - private void updateBuyInfo(String symbol, TextField qtyField, Label label) { - try { - if (symbol == null || symbol.isEmpty()) return; - Stock s = exchange.getStock(symbol.toUpperCase()); - if (s == null) return; - BigDecimal qty = new BigDecimal(qtyField.getText()); - BigDecimal gross = s.getSalesPrice().multiply(qty); - BigDecimal comm = gross.multiply(new BigDecimal("0.005")); - label.setText("Gross: $" + formatMoney(gross) + - " | Commission: $" + formatMoney(comm) + - " | Total: $" + formatMoney(gross.add(comm))); - } catch (Exception e) { - // ignore partial input - } - } + static class PortfolioRow { + Share s; - private void updateStatus() { - PlayerStatus playerStatus = player.getStatus(); - statusLabel.setText( - "Player: " + player.getName() + - " | Status: " + playerStatus.getDisplayName() + - " | Week: " + exchange.getWeek() + - " | Cash: $" + formatMoney(player.getMoney()) + - " | Net Worth: $" + formatMoney(getNetWorth()) - ); + PortfolioRow(Share s) { + this.s = s; + } + + public String getSymbol() { + return s.getStock().getSymbol(); } - private void refreshAllUI() { - if (portfolioTable != null) { - updatePortfolio(portfolioTable); - } - if (holdingsList != null) { - updateHoldingsList(holdingsList); - } - if (historyTable != null) { - updateHistory(historyTable, - weekFilterCombo != null && weekFilterCombo.getValue() != null - ? weekFilterCombo.getValue() : null); - if (weekFilterCombo != null) { - updateWeekCombo(weekFilterCombo); - } - } + public String getQty() { + return s.getQuantity().toString(); } - private BigDecimal getNetWorth() { - return player.getMoney().add(player.getPortfolio().getNetWorth()); + public String getBuyPrice() { + return "$" + s.getPurchasePrice(); } - private void showConfirmation(String title, Transaction t) { - StringBuilder msg = new StringBuilder(); - if (t instanceof Purchase) { - Purchase p = (Purchase) t; - msg.append("Bought ").append(formatMoney(p.getShare().getQuantity())) - .append(" @ $").append(formatMoney(p.getShare().getPurchasePrice())) - .append("\nCost: $").append(formatMoney(p.getCalculator().calculateTotal())); - } else { - Sale s = (Sale) t; - msg.append("Sold ").append(formatMoney(s.getShare().getQuantity())) - .append(" @ $").append(formatMoney(s.getShare().getStock().getSalesPrice())) - .append("\nProceeds: $").append(formatMoney(s.getCalculator().calculateTotal())); - } - alert(title, msg.toString()); + public String getCurrentPrice() { + return "$" + s.getStock().getSalesPrice(); } - private void alert(String title, String msg) { - Alert a = new Alert(Alert.AlertType.INFORMATION); - a.setTitle(title); - a.setHeaderText(null); - a.setContentText(msg); - a.showAndWait(); + public String getGainLoss() { + SaleCalculator calc = new SaleCalculator(s); + BigDecimal val = calc.calculateTotal(); + BigDecimal cost = s.getPurchasePrice().multiply(s.getQuantity()); + return "$" + val.subtract(cost); } + } + + static class HistoryRow { + Transaction t; - private boolean confirm(String title, String msg) { - Alert a = new Alert(Alert.AlertType.CONFIRMATION); - a.setTitle(title); - a.setHeaderText(null); - a.setContentText(msg); - return a.showAndWait().get() == ButtonType.OK; + HistoryRow(Transaction t) { + this.t = t; } - private String formatMoney(BigDecimal n) { - return n.setScale(2, java.math.RoundingMode.HALF_UP).toString(); + public String getWeek() { + return String.valueOf(t.getWeek()); } - public Scene getScene() { - return scene; + public String getType() { + return t instanceof Purchase ? "BUY" : "SELL"; } - // ---- Data row classes ---- - - static class StockRow { - Stock s; - StockRow(Stock s) { this.s = s; } - public String getSymbol() { return s.getSymbol(); } - public String getCompany() { return s.getCompany(); } - public String getPrice() { return "$" + s.getSalesPrice(); } - public String getChange() { return "$" + s.getLatestPriceChange(); } - public String getHigh() { return "$" + s.getHighestPrice(); } - public String getLow() { return "$" + s.getLowestPrice(); } + public String getSymbol() { + return t.getShare().getStock().getSymbol(); } - static class PortfolioRow { - Share s; - PortfolioRow(Share s) { this.s = s; } - public String getSymbol() { return s.getStock().getSymbol(); } - public String getQty() { return s.getQuantity().toString(); } - public String getBuyPrice() { return "$" + s.getPurchasePrice(); } - public String getCurrentPrice() { return "$" + s.getStock().getSalesPrice(); } - public String getGainLoss() { - SaleCalculator calc = new SaleCalculator(s); - BigDecimal val = calc.calculateTotal(); - BigDecimal cost = s.getPurchasePrice().multiply(s.getQuantity()); - return "$" + val.subtract(cost); - } + public String getQty() { + return t.getShare().getQuantity().toString(); } - static class HistoryRow { - Transaction t; - HistoryRow(Transaction t) { this.t = t; } - public String getWeek() { return String.valueOf(t.getWeek()); } - public String getType() { return t instanceof Purchase ? "BUY" : "SELL"; } - public String getSymbol() { return t.getShare().getStock().getSymbol(); } - public String getQty() { return t.getShare().getQuantity().toString(); } - public String getTotal() { return "$" + t.getCalculator().calculateTotal(); } + public String getTotal() { + return "$" + t.getCalculator().calculateTotal(); } + } } diff --git a/src/main/java/View/StockTradingGameApp.java b/src/main/java/View/StockTradingGameApp.java index 838df67..b1ab987 100644 --- a/src/main/java/View/StockTradingGameApp.java +++ b/src/main/java/View/StockTradingGameApp.java @@ -6,7 +6,9 @@ import javafx.stage.Stage; /** - * StockTradingGameApp class. + * Main JavaFX application class for the Stock Trading Game. + * Manages the application lifecycle, window setup, scene transitions, + * and exchanges between the game setup and main game views. */ public class StockTradingGameApp extends Application { private Stage primaryStage; @@ -14,6 +16,11 @@ public class StockTradingGameApp extends Application { private Player player; private StockFileHandler fileHandler; + /** + * Initializes and displays the primary stage. + * + * @param primaryStage the primary window stage provided by JavaFX + */ @Override public void start(Stage primaryStage) { this.primaryStage = primaryStage; @@ -27,11 +34,19 @@ public void start(Stage primaryStage) { primaryStage.show(); } + /** + * Displays the game setup scene where players configure their game. + */ private void showGameSetup() { GameSetupScene setupScene = new GameSetupScene(this::startGame); primaryStage.setScene(setupScene.getScene()); } + /** + * Starts the main game scene after setup is complete. + * + * @param gameData the game configuration data from the setup scene + */ private void startGame(GameSetupScene.StartGameData gameData) { this.exchange = gameData.exchange; this.player = new Player(gameData.playerName, gameData.startingCapital); @@ -40,10 +55,18 @@ private void startGame(GameSetupScene.StartGameData gameData) { primaryStage.setScene(gameScene.getScene()); } + /** + * Ends the game and closes the application. + */ private void endGame() { primaryStage.close(); } + /** + * Main method to launch the JavaFX application. + * + * @param args command-line arguments (not used) + */ public static void main(String[] args) { launch(args); } diff --git a/target/test-classes/ExchangeTest.class b/target/test-classes/ExchangeTest.class index 238ca9dd2248bbe4e28a26a8a964a5ec95435ded..5b02574e2280eca7a62f88929280c7d347ae094e 100644 GIT binary patch literal 10397 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 CFx9dE diff --git a/target/test-classes/PortfolioTest.class b/target/test-classes/PortfolioTest.class index 679e0c4d7ea6495c5d426472e08b43044c6df084..45ae58688b9e7efb5e87f84abf6f429fffc62b8b 100644 GIT binary patch literal 5661 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 P2}Att_zpkdCp7#EbX!Al literal 4255 zcmbVO3s)0I7`=l)LRb`|q9}p_Di1NB(yBpgKtyZ=1Vph~x`ZKF*zCs52DR_^`~7bJ zMSI%f(R2C(ditAs`pst9Ou{DAat@pMc4qFq^UeKc_OE{){|Vqceh#4u+XARoP=i{A z#uarIJr_a% zd&Gz)hVwonHqXhwDPbg}I_yJp0Q(gjKnp|NOiH!*EYH&7YO-Px3=MHY08FJH4M z%e+tdnvwH0qEj-XPXX*^%BegD(V+{2Auc3eV>r_l_lz5T8qHxU@pu^9#G_g~YCy$0 zLq_f!BuC#MXZH;TaZ@V- z`g%#6C@XH?$S^cd=g13bKBr|hni4Y%(^g#{x0lJzFkdz;KB^{CQHBFHm8rAr5i-AR z2SA;aHr3|#FdFoaK=vbj9+^}HW-R_$rUlOc%UG(lYx6GKXoA>z= z!!FS+YSO8-4O=vAyxo8=>JR{>9s8XbWMNxdXjt$H4aNBpBU&VklHCy9 zp&64-8-7_6j^6H7)CsXTI*o=+(J5)b6Oot0B5VUQ@6}Q4QV@(VGf# zPV}~j-cevX(R(6#UqRN1J`~YM3M?o3L`0t|Fes|U=OKK7F9Y~W!Poev=mgV@HFK3m z;?7o)5OrACLm zGf!E0u6)Y5$RjEh3|(}hF2@anTh7tR$#UN_94T0?NJ#PW0{D@kwenE+c&hiMb4 z=qjLNPkawhYZtZY>xZ7VD2mW8qu-jIb+BKkQH6PWif*7Dx9O?m2jB(Vp$NE3y;aWS zF$A5y=AI`A%!lj3^-s_+AKtN!o$J{BJN6a;#g`YblhlMI&}J9(Md}lj3j!Z6c?1je zRV3zhJi)>Fp5L(7!EYmcr*YUJ>WKF~!qEcNb~^k)M6GB>JN7$tO?eoXu9Xs?Vvz_p zh!cVrOCIsEOH2!^L@Xrr{(<9<&|k{mO8jl;#vzHn$KzLg`MJlx>heb_^Lyza5vP4b zbdrc8^jUON5)tu4?DQ3}{J%tqtsoI+d_?q+h+YzL{3%2vJrN5|tJDHfqWcx+*U=HD zpEAS)WP+1qfm0H3hr9ZHWNtqTq&(ttWEoKeN;viyBfuk!-7IW!-p^+VWynF#7m4Dc z7u!nnFrzMJLpav+7|;8IUZjc}kw6=KL08IvPWXe45$M$|L08LwPWppR5a{(SLG?1A zQ~sbg2=vyLpy@K8*Zo275a>c>(8elpRnZ2CV{x47Pi%ghfBjA84<5-6`2+K=MA7DM zn@KR$1ePHJx*1)97_|feooFQo3?ox-0-lqA_xb?S1zb`2ED3fFi@7q&CnW+u<#gY> z1S4*0nm3eF0ak>*B0+B)_;OO{!@^5SKI>jmSViEN1iU#ZA@Tj<0-q+$;;%($&VzcF zh@257U4G}_iKGkVKFzMe13aYP3xxX_y?Yt2(0{)5y4QNkYrX5WKJZ!}Q|nv82+{6; Khad10n*RmEn4S0l diff --git a/target/test-classes/StockTest.class b/target/test-classes/StockTest.class index 0008700068cbbe4a7129761956c9351e88fe7dbe..e22183d1d0208cb1c72e53109d70a846ae53f6ee 100644 GIT binary patch literal 6116 zcmb_f`Fm8=8GdgPGLzvFV3HuQN+SeGf?*N}3QVvjAwqOWkc3cB+ulrWCKo1i$60`2 zUD~?TYHh2vt(LZ0YxmVE2()(Jtljs0-~Nd{eb2df?!7Z}Nbq?&KTOVi=bZO@zx8~1 z>A&Y*1h5nT386+q!+60?9-T1rg%IjAtT!vg4fXw2#wuK^p|+!QQbXMkcO$69QXL_LH7rk98FREaH)ZA~jH$G#A)2t0Mtai7 zS>m@VtSih~c@0Yw9uZ8xMmmhkc)Yi#rze6|tPzI2T0=`m!uQg6A!lV$@y^Kzns9|Uu~x%Q#fj=W zj-)~*ks6?(UB?pC)9D39X17@wpP!qu(;6vinL9@dOe zag2HE`lgba@WyHtvf!d&jmvwF#hRo2Mp}9yKf)ZhjI>ai(6JYz8k*>h%44jsl23{S zD;-M^CcYH=us?*G7zUZO-j0rOOwgRXb&|EoA|4SbF)4z5kfqTvGSWFIE+5iy7&r3- zHHzdP2_};pWep)#F*(oE9ZnK>dRWe@$_sx!TPl9nP2SmlbxmALjCB|A&AiWB0elwI>jbpnPL+Z zV8;ozU06fE*L6#~+b=|6ER&ue%b3y;4Qyv`)X|SSL~vBCujKK{92?2+GH1+8p6Z9} z?0gt^Q}cFyM5?@5$6Ig@Ik6%t`edmc9Ck}JT}XCKdpa6enR-&aS{XJTl{68 zzuX$er+KHl*B4#wFRuyX^GsmY61j}q#az<7-V(!0qc@*y5|r5I25q~LFXW8uUb8T3 zPv;|e9%n*0U6#jI=9qodj3uOjJZQ`qNzo^DynwSDPUd`(hL?lc=iYR{_ZXRJK8o!L z+df*%2H#nC013RjWfv#dY=g%ye$!F8@ZDg-yU0TZ3!B4Oe0e>dTY+$dS^i}mPa;gg zmnd*$U)S*wDf^l)`;Lx}N!ho3*~>aUE@j{MWk1pJ2`T%rFZ-pAPfFP@eA(}Ge1@_R ze(THrtmCs%_9x0%|09`X4}NRJDq&7KDw?5h>n zYxx?l3t%^Ugm);hrx-ioy(^`cufC%hjUPorF0qs)R0Xh8?ob5Iw4U*#aV z8e15V0XOi2O5KyFlXkTWbEK*v>OGBNZ>_S^(cI0n*+ffY0bAQWTSqQZpRlu}PpmJj zZwvMHQQuajz8jSKPS8ivw+`%$iMowvF{ZI!9*`D_T;5oF7O$Wb^ zj_w#>=?|g@LjjlcdLc+E2^?_=^mz)WD+tUuzomo3oIgDTnZU}Jb z^Ee#+KX4Fy91dv(hf#7EBZqzDaFddQt>jRh`febk-C0j(u1u$s`Uj|UlBswwpwCbJ z;zjB^7Vz_l1@+xbeYa5GtxA0-mHPVK=GyoyZg;#_Zi&;f4I)kjP#e9--Kj*qo!KJ_ zs?&kIF|kp)>n_qii#OHaV4b@WOxs{h%@8)lcVd=Z%aV$np4I6p4)cAhtSKRJYU*Ak zsU5DVdi1_?cq>@S4;}Q&#WB>zQse@L>YkzRTpaUVYRqHAoL6E#vN-0WYRo%`dAAbt z-HT&BrpCO7nD;9&-?KR8d)1f^67yjt=KB`Md|ZwB2r(a1Vm_h7^yi2By3XPKYS72o zm`?>j{rTa87XkVqHRzKBeYzTSMMTV|%*Ll!BI0HJupFfN_`-`1r-!CPnma~`4#P3Z z&r@eW%Su$17_m6hR$*;*v0CJ`@XW$s#{*y-hO5BJh(%j8uk|U2-5J2*5M6~;POK&R z%tB!I2EaIdSAlhNvK60YrO8uMFnTBe!g0L{q>_;{zJ5yW_eVXj8ZqZrfo-8gBcle< z-!x7vm4SOg0yvX(gVS7A)QEo%s-WWT3%pW5NiThloD`5x1t3?t{i+s@;%D(ive{4g zbDVL`<3)T$uD*(I;G1&wEqoW>ldG5T1N=~~euSUmXL9v({0hI8tKZ=F_=8;i5r4s7 R@i*d%f3*INf8yV0{U0rAvtj-}Rh%{XbV< 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 s