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 238ca9d..5b02574 100644 Binary files a/target/test-classes/ExchangeTest.class and b/target/test-classes/ExchangeTest.class differ diff --git a/target/test-classes/PortfolioTest.class b/target/test-classes/PortfolioTest.class index 679e0c4..45ae586 100644 Binary files a/target/test-classes/PortfolioTest.class and b/target/test-classes/PortfolioTest.class differ diff --git a/target/test-classes/ShareTest.class b/target/test-classes/ShareTest.class index abf202d..65bc1e8 100644 Binary files a/target/test-classes/ShareTest.class and b/target/test-classes/ShareTest.class differ diff --git a/target/test-classes/StockTest.class b/target/test-classes/StockTest.class index 0008700..e22183d 100644 Binary files a/target/test-classes/StockTest.class and b/target/test-classes/StockTest.class differ