From 19611bd6935ca2c58e0f442f909ebcdb7e7bab02 Mon Sep 17 00:00:00 2001 From: = Date: Mon, 25 May 2026 11:38:02 +0200 Subject: [PATCH] Feat: Basic improvements, testing. --- .../idatt2003/g40/mappe/engine/Exchange.java | 119 ++++++++----- .../idatt2003/g40/mappe/model/Portfolio.java | 4 +- .../idi/idatt2003/g40/mappe/model/Stock.java | 4 +- .../view/widgets/market/MarketController.java | 2 +- .../g40/mappe/engine/ExchangeTest.java | 165 ++++++++++++++---- 5 files changed, 214 insertions(+), 80 deletions(-) diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/Exchange.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/Exchange.java index b73f634..a3a9af3 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/Exchange.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/Exchange.java @@ -1,31 +1,25 @@ package edu.ntnu.idi.idatt2003.g40.mappe.engine; import edu.ntnu.idi.idatt2003.g40.mappe.model.Player; -import edu.ntnu.idi.idatt2003.g40.mappe.model.Purchase; -import edu.ntnu.idi.idatt2003.g40.mappe.model.Sale; import edu.ntnu.idi.idatt2003.g40.mappe.model.Share; import edu.ntnu.idi.idatt2003.g40.mappe.model.Stock; import edu.ntnu.idi.idatt2003.g40.mappe.model.Transaction; import edu.ntnu.idi.idatt2003.g40.mappe.service.*; -import edu.ntnu.idi.idatt2003.g40.mappe.service.event.EventData; -import edu.ntnu.idi.idatt2003.g40.mappe.service.event.EventManager; -import edu.ntnu.idi.idatt2003.g40.mappe.service.event.EventPublisher; -import edu.ntnu.idi.idatt2003.g40.mappe.service.event.EventType; import edu.ntnu.idi.idatt2003.g40.mappe.utils.Validator; -import javafx.beans.property.IntegerProperty; -import javafx.beans.property.SimpleIntegerProperty; - import java.math.BigDecimal; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Random; +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.SimpleIntegerProperty; /** * Represents a stock exchange where stocks can be traded. * - *

Holds a map of stocks where stock symbol is key and stock is value.

+ *

Holds a map of {@link Stock} objects where stock symbol is key and + * stock is value.

* *

Delegates buying and selling to player elements using calculators

* @@ -33,6 +27,8 @@ * * @see Player * @see TransactionCalculator + * + * @version 1.0.0 * */ public final class Exchange { @@ -59,13 +55,16 @@ public final class Exchange { /** * Constructor. * - * @param name name of exchange. - * @param stocks list of {@link Stock} objects. + * @param name name of exchange. + * @param stocks list of {@link Stock} objects. * * @throws IllegalArgumentException if name or stocks are empty/null. * */ - public Exchange(final String name, final List stocks) throws IllegalArgumentException { - if (!Validator.NOT_EMPTY.isValid(name) || stocks == null || stocks.isEmpty()) { + public Exchange(final String name, final List stocks) + throws IllegalArgumentException { + if (!Validator.NOT_EMPTY.isValid(name) + || stocks == null + || stocks.isEmpty()) { throw new IllegalArgumentException("Invalid exchange parameters!"); } this.name = name; @@ -108,7 +107,7 @@ public IntegerProperty weekProperty() { * * @param symbol the stock symbol. * - * @return true or false. + * @return true or false. * */ public boolean hasStock(final String symbol) { return stockMap.containsKey(symbol); @@ -117,16 +116,16 @@ public boolean hasStock(final String symbol) { /** * Getter method for stock element. * - * @param symbol the symbol of the stock to get. + * @param symbol the symbol of the stock to get. * - * @return {@link Stock} element gotten. + * @return {@link Stock} element gotten. * * @throws IllegalArgumentException if symbol is invalid. * */ public Stock getStock(final String symbol) throws IllegalArgumentException { - if (!Validator.VALID_STOCK_NAME.isValid(symbol)) { + if (!Validator.VALID_STOCK_SYMBOL.isValid(symbol)) { throw new IllegalArgumentException( - Validator.VALID_STOCK_NAME.getErrorMessage()); + Validator.VALID_STOCK_SYMBOL.getErrorMessage()); } return stockMap.get(symbol); } @@ -136,7 +135,7 @@ public Stock getStock(final String symbol) throws IllegalArgumentException { * * @param searchTerm the term to search for. * - * @return a list of {@link Stock} objects. + * @return a list of {@link Stock} objects. * */ public List findStocks(final String searchTerm) { List result = new ArrayList<>(); @@ -154,24 +153,26 @@ public List findStocks(final String searchTerm) { /** * Method called when a player buys a stock. * - * @param symbol the stock this player buys. - * @param quantity the amount of stock to buy. - * @param player the player buying stock. + * @param symbol the stock this player buys. + * @param quantity the amount of stock to buy. + * @param player the player buying stock. * - * @return Transaction representing the transaction. + * @return Transaction representing the transaction. * * @throws IllegalArgumentException if symbol or player is invalid. * */ public Transaction buy(final String symbol, final BigDecimal quantity, final Player player) throws IllegalArgumentException { - if (!Validator.VALID_STOCK_NAME.isValid(symbol) || player == null) { + if (!Validator.VALID_STOCK_SYMBOL.isValid(symbol) || player == null) { throw new IllegalArgumentException("Invalid purchase!"); } Stock stock = stockMap.get(symbol); Share share = new Share(stock, quantity, stock.getSalesPrice()); TransactionCalculator calculator = new PurchaseCalculator(share); - Purchase purchase = new Purchase(share, week.get(), calculator); + Transaction purchase = TransactionFactory.createTransaction( + TransactionType.PURCHASE, share, getWeek(), calculator + ); player.handleTransaction(purchase); return purchase; } @@ -179,10 +180,10 @@ public Transaction buy(final String symbol, /** * Method called when a player sells share. * - * @param share the share to sell. - * @param player the player buying stock. + * @param share the share to sell. + * @param player the player buying stock. * - * @return Transaction representing the transaction. + * @return Transaction representing the transaction. * * @throws IllegalArgumentException if share or player is null. * */ @@ -192,27 +193,37 @@ public Transaction sell(final Share share, final Player player) throw new IllegalArgumentException("Invalid sell!"); } TransactionCalculator calculator = new SaleCalculator(share); - Sale sale = new Sale(share, week.get(), calculator); + Transaction sale = TransactionFactory.createTransaction( + TransactionType.SALE, share, getWeek(), calculator + ); player.handleTransaction(sale); return sale; } /** - * Method called when a player sells share. + * Method called when a player sells share, + * defined by an amount instead of specific {@link Share} object. + * + *

Might split shares into multiples to ensure proper selling. + * Uses the {@link edu.ntnu.idi.idatt2003.g40.mappe.model.Portfolio} + * to split.

* - * @param amount the amount of "shares" to sell. + * @param amount the amount of "shares" to sell. * @param stockSymbol the stock to sell shares in. - * @param player the player buying stock. + * @param player the player buying stock. * - * @return Transaction representing the transaction. + * @return List of transactions commited to sell the shares. * - * @throws IllegalArgumentException if any parameter is null, or if player does not have enough shares. + * @throws IllegalArgumentException if any parameter is null, + * or if player does not have enough shares. * */ public List sell(BigDecimal amount, final String stockSymbol, final Player player) throws IllegalArgumentException { - if (amount == null || player == null || !Validator.NOT_EMPTY.isValid(stockSymbol)) { + if (amount == null + || player == null + || !Validator.VALID_STOCK_SYMBOL.isValid(stockSymbol)) { throw new IllegalArgumentException("Invalid sell!"); } else { @@ -220,7 +231,8 @@ public List sell(BigDecimal amount, .filter(s -> s.getStock().getSymbol().equals(stockSymbol)) .toList(); - BigDecimal totalOwned = player.getPortfolio().getTotalSharesBySymbol(stockSymbol); + BigDecimal totalOwned = player.getPortfolio() + .getTotalSharesBySymbol(stockSymbol); if (amount.compareTo(totalOwned) > 0) { amount = totalOwned; @@ -239,7 +251,9 @@ public List sell(BigDecimal amount, remainingToSell = remainingToSell.subtract(shareQty); transactions.add(sell(share, player)); } else { - Share newShare = player.getPortfolio().splitShare(share, remainingToSell); + Share newShare = player.getPortfolio().splitShare( + share, remainingToSell + ); remainingToSell = BigDecimal.ZERO; transactions.add(sell(newShare, player)); } @@ -250,12 +264,19 @@ public List sell(BigDecimal amount, /** * Method for advancing time, increasing the amount of weeks. + * + *

Applies a random price change from -5% to 5% to every stock, + * plus a flat percent determined by their fortune, that can range from + * -10% to +10%.

+ * + * @see edu.ntnu.idi.idatt2003.g40.mappe.view.widgets.minigames.GameEngineView * */ public void advance() { for (Stock stock : stockMap.values()) { BigDecimal currentPrice = stock.getSalesPrice(); - double change = ((random.nextDouble() * 0.10) - 0.05) + stock.getFortune(); + double change = ( + (random.nextDouble() * 0.10) - 0.05) + stock.getFortune(); stock.setFortune(0); BigDecimal factor = BigDecimal.valueOf(1 + change); @@ -269,11 +290,17 @@ public void advance() { * Method for getting the stocks with the most * amount of increase since last week. * - * @param limit the maximum amount of stocks returned + * @param limit the maximum amount of stocks returned * * @return list of {@link Stock} objects. + * + * @throws IllegalArgumentException if limit is invalid (negative or zero). * */ - public List getGainers(final int limit) { + public List getGainers(final int limit) + throws IllegalArgumentException { + if (limit < 1) { + throw new IllegalArgumentException("Invalid limit for getting gainers!"); + } return stockMap.entrySet().stream() // We only want the stocks with a positive price change. .filter(e -> @@ -294,11 +321,17 @@ public List getGainers(final int limit) { * Method for getting the stocks with the highest * loss of price since last week. * - * @param limit the maximum amount of stocks returned + * @param limit the maximum amount of stocks returned * * @return list of {@link Stock} objects. + * + * @throws IllegalArgumentException if limit is invalid (negative or zero). * */ - public List getLosers(final int limit) { + public List getLosers(final int limit) + throws IllegalArgumentException { + if (limit < 1) { + throw new IllegalArgumentException("Invalid limit for getting losers!"); + } return stockMap.entrySet().stream() // Only get entries with negative price change. .filter(e -> diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Portfolio.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Portfolio.java index 483adb8..0c7ea5e 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Portfolio.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Portfolio.java @@ -82,9 +82,9 @@ public List getShares() { * @throws IllegalArgumentException if symbol is invalid. */ public List getShares(final String symbol) throws IllegalArgumentException { - if (!Validator.VALID_STOCK_NAME.isValid(symbol)) { + if (!Validator.VALID_STOCK_SYMBOL.isValid(symbol)) { throw new IllegalArgumentException( - Validator.VALID_STOCK_NAME.getErrorMessage()); + Validator.VALID_STOCK_SYMBOL.getErrorMessage()); } return shares.stream() .filter(s -> symbol.equalsIgnoreCase(s.getStock().getSymbol())) diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Stock.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Stock.java index 1ca9664..0aff105 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Stock.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Stock.java @@ -44,9 +44,9 @@ public final class Stock { public Stock(final String symbol, final String company, final BigDecimal salesPrice) throws IllegalArgumentException { - if (!Validator.VALID_STOCK_NAME.isValid(symbol)) { + if (!Validator.VALID_STOCK_SYMBOL.isValid(symbol)) { throw new IllegalArgumentException( - Validator.VALID_STOCK_NAME.getErrorMessage()); + Validator.VALID_STOCK_SYMBOL.getErrorMessage()); } else { this.symbol = symbol; this.company = company; diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/market/MarketController.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/market/MarketController.java index e185fb3..86838a9 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/market/MarketController.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/market/MarketController.java @@ -84,7 +84,7 @@ protected void initInteractions() { * @return the total quantity, or 0 if the symbol is invalid. * */ private int ownedQuantity(final String symbol) { - if (!Validator.VALID_STOCK_NAME.isValid(symbol)) { + if (!Validator.VALID_STOCK_SYMBOL.isValid(symbol)) { return 0; } return player.getPortfolio().getShares(symbol).stream() diff --git a/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/ExchangeTest.java b/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/ExchangeTest.java index 8f4f1a5..e4dd5ee 100644 --- a/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/ExchangeTest.java +++ b/src/test/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/ExchangeTest.java @@ -4,98 +4,183 @@ import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import edu.ntnu.idi.idatt2003.g40.mappe.model.Player; +import edu.ntnu.idi.idatt2003.g40.mappe.model.Purchase; +import edu.ntnu.idi.idatt2003.g40.mappe.model.Sale; +import edu.ntnu.idi.idatt2003.g40.mappe.model.Share; +import edu.ntnu.idi.idatt2003.g40.mappe.model.Stock; +import edu.ntnu.idi.idatt2003.g40.mappe.model.Transaction; import java.math.BigDecimal; import java.util.List; -import edu.ntnu.idi.idatt2003.g40.mappe.model.*; +import edu.ntnu.idi.idatt2003.g40.mappe.service.PurchaseCalculator; +import edu.ntnu.idi.idatt2003.g40.mappe.service.SaleCalculator; +import edu.ntnu.idi.idatt2003.g40.mappe.service.TransactionCalculator; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; final class ExchangeTest { + /** + * Test sample stock. + * */ private Stock appleStock; + /** + * Test sample stock. + * */ + private Stock teslaStock; + + /** + * Test sample stock. + * */ + private Stock pearStock; + + /** + * Test exchange to use. + * */ + private Exchange testExchange; + + /** + * Test player for testing transactions. + * */ + private Player testPlayer; + @BeforeEach void setUp() { appleStock = new Stock("AAPL", "Apple", new BigDecimal("100")); + teslaStock = new Stock("TSLA", "Tesla", new BigDecimal("200")); + pearStock = new Stock("Pear", "Pear Inc.", new BigDecimal("97")); + testExchange = new Exchange("NASDAQ", List.of(appleStock, teslaStock)); + testPlayer = new Player("Alice", new BigDecimal("1000")); } @Test void constructorSetsNameWeekAndStocksCorrectly() { - Stock tesla = new Stock("TSLA", "Tesla", new BigDecimal("200")); - - Exchange exchange = new Exchange("NASDAQ", List.of(appleStock, tesla)); - - assertEquals("NASDAQ", exchange.getName()); - assertEquals(1, exchange.getWeek()); - assertTrue(exchange.hasStock("AAPL")); - assertTrue(exchange.hasStock("TSLA")); + assertEquals("NASDAQ", testExchange.getName()); + assertEquals(1, testExchange.getWeek()); + assertTrue(testExchange.hasStock("AAPL")); + assertTrue(testExchange.hasStock("TSLA")); } @Test void getStockReturnsCorrectStock() { - Exchange exchange = new Exchange("NASDAQ", List.of(appleStock)); - - Stock result = exchange.getStock("AAPL"); + Stock result = testExchange.getStock("AAPL"); assertSame(appleStock, result); } @Test void findStocksReturnsMatchingStocksBySymbolOrCompany() { - Stock tesla = new Stock("TSLA", "Tesla", new BigDecimal("200")); - Exchange exchange = new Exchange("NASDAQ", List.of(appleStock, tesla)); - - List resultBySymbol = exchange.findStocks("AAP"); - List resultByCompany = exchange.findStocks("tes"); + List resultBySymbol = testExchange.findStocks("AAP"); + List resultByCompany = testExchange.findStocks("tes"); assertEquals(1, resultBySymbol.size()); assertTrue(resultBySymbol.contains(appleStock)); assertEquals(1, resultByCompany.size()); - assertTrue(resultByCompany.contains(tesla)); + assertTrue(resultByCompany.contains(teslaStock)); } @Test void buyReturnsPurchaseAndWithdrawsMoneyFromPlayer() { - Exchange exchange = new Exchange("NASDAQ", List.of(appleStock)); - Player player = new Player("Alice", new BigDecimal("1000")); + Transaction transaction = + testExchange.buy("AAPL", new BigDecimal("2"), testPlayer); + + TransactionCalculator calculator = + new PurchaseCalculator(transaction.getShare()); - Transaction transaction = exchange.buy("AAPL", new BigDecimal("2"), player); + BigDecimal expectedPlayerMoney = + testPlayer.getStartingMoney().subtract(calculator.calculateTotal()); assertInstanceOf(Purchase.class, transaction); assertEquals(1, transaction.getWeek()); assertEquals(new BigDecimal("2"), transaction.getShare().getQuantity()); - assertEquals(new BigDecimal("100"), transaction.getShare() + assertEquals(appleStock.getSalesPrice(), transaction.getShare() .getPurchasePrice()); - assertEquals(new BigDecimal("799.000"), player.getMoney()); + assertEquals(expectedPlayerMoney, testPlayer.getMoney()); } @Test void sellReturnsSaleAndAddsMoneyToPlayer() { - Stock apple = new Stock("AAPL", "Apple", new BigDecimal("150")); - Exchange exchange = new Exchange("NASDAQ", List.of(apple)); - Player player = new Player("Alice", new BigDecimal("1000")); - Share share = new Share(apple, new BigDecimal("2"), new BigDecimal("100")); + Share share = + new Share(appleStock, new BigDecimal("2"), appleStock.getSalesPrice()); - Transaction transaction = exchange.sell(share, player); + Transaction transaction = testExchange.sell(share, testPlayer); + + TransactionCalculator calculator = new SaleCalculator(share); + BigDecimal expectedPlayerMoney = + testPlayer.getStartingMoney().add(calculator.calculateTotal()); assertInstanceOf(Sale.class, transaction); assertEquals(1, transaction.getWeek()); assertSame(share, transaction.getShare()); - assertEquals(new BigDecimal("1267.9000"), player.getMoney()); + assertEquals(expectedPlayerMoney, testPlayer.getMoney()); + } + + @Test + void sellingAmountOfSharesRequiringSplitFunctions() { + Transaction purchase = testExchange.buy( + appleStock.getSymbol(), new BigDecimal("3"), testPlayer + ); + + List sales = testExchange.sell( + new BigDecimal("1.5"), + appleStock.getSymbol(), + testPlayer + ); + + SaleCalculator saleCalculator = new SaleCalculator(sales.getFirst().getShare()); + PurchaseCalculator purchaseCalculator = new PurchaseCalculator(purchase.getShare()); + BigDecimal expectedPlayerMoney = testPlayer.getStartingMoney() + .subtract(purchaseCalculator.calculateTotal()) + .add(saleCalculator.calculateTotal()); + + assertEquals(1, sales.size()); + + assertEquals(expectedPlayerMoney, testPlayer.getMoney()); + + BigDecimal expectedShareAmountOwned = purchase.getShare().getQuantity() + .subtract(sales.getFirst().getShare().getQuantity()); + + assertEquals(expectedShareAmountOwned, + testPlayer + .getPortfolio() + .getShares(appleStock.getSymbol()) + .getFirst() + .getQuantity() + ); + } + + @Test + void sellingAmountOfSharesRequiringSplitFunctions2() { + // Arrange: Clear setup with explicit numbers + Transaction purchase1 = testExchange.buy(appleStock.getSymbol(), new BigDecimal("3.12"), testPlayer); + Transaction purchase2 = testExchange.buy(appleStock.getSymbol(), new BigDecimal("4.56"), testPlayer); + + // Act: Sell 6 shares (requires taking 3.12 from purchase1 and 2.88 from purchase2) + List sales = testExchange.sell(new BigDecimal("6.00"), appleStock.getSymbol(), testPlayer); + + // Assert 1: Verify the split logic actually happened + assertEquals(2, sales.size(), "Should split the sale across two purchase records"); + assertEquals(new BigDecimal("3.12"), sales.get(0).getShare().getQuantity()); + assertEquals(new BigDecimal("2.88"), sales.get(1).getShare().getQuantity()); + + // Assert 2: Verify player money matches a known, pre-calculated constant + BigDecimal expectedFinalMoney = new BigDecimal("822.16000"); // Replace with the actual hardcoded math result + assertEquals(expectedFinalMoney, testPlayer.getMoney()); } @Test void advanceIncreasesWeekAndUpdatesStockPrice() { - Exchange exchange = new Exchange("NASDAQ", List.of(appleStock)); BigDecimal oldPrice = appleStock.getSalesPrice(); - exchange.advance(); + testExchange.advance(); - assertEquals(2, exchange.getWeek()); + assertEquals(2, testExchange.getWeek()); assertNotEquals(oldPrice, appleStock.getSalesPrice()); } @@ -146,4 +231,20 @@ void getLosersActuallyReturnsProperLosers() { boolean actualLosersNotContainingLoserOutsideOfLimit = !actualLosers.contains(pearStock); assertTrue(actualLosersNotContainingLoserOutsideOfLimit); } + + @Test + void invalidLimitForGettingGainersThrowError() { + Exchange exchange = new Exchange("Exchange", List.of(appleStock)); + + assertThrows(IllegalArgumentException.class, () -> exchange.getGainers(0)); + assertThrows(IllegalArgumentException.class, () -> exchange.getGainers(-1)); + } + + @Test + void invalidLimitForGettingLosersThrowError() { + Exchange exchange = new Exchange("Exchange", List.of(appleStock)); + + assertThrows(IllegalArgumentException.class, () -> exchange.getLosers(0)); + assertThrows(IllegalArgumentException.class, () -> exchange.getLosers(-1)); + } }