From 043369c5aa0cbe30ff1c6a441aca069c08ac029d Mon Sep 17 00:00:00 2001 From: Nikollai Date: Wed, 8 Apr 2026 15:56:54 +0200 Subject: [PATCH 01/16] Created 3 files: - TransactionCalculatorFactory - PurchaseCalculatorFactory - SaleCalculatorFactory --- .../calculators/PurchaseCalculatorFactory.java | 18 ++++++++++++++++++ .../calculators/SaleCalculatorFactory.java | 16 ++++++++++++++++ .../TransactionCalculatorFactory.java | 8 ++++++++ 3 files changed, 42 insertions(+) create mode 100644 src/main/java/millions/calculators/PurchaseCalculatorFactory.java create mode 100644 src/main/java/millions/calculators/SaleCalculatorFactory.java create mode 100644 src/main/java/millions/calculators/TransactionCalculatorFactory.java diff --git a/src/main/java/millions/calculators/PurchaseCalculatorFactory.java b/src/main/java/millions/calculators/PurchaseCalculatorFactory.java new file mode 100644 index 0000000..7f496cf --- /dev/null +++ b/src/main/java/millions/calculators/PurchaseCalculatorFactory.java @@ -0,0 +1,18 @@ +package millions.calculators; + +import millions.Share; + +public class PurchaseCalculatorFactory extends TransactionCalculatorFactory { + Share share; + + @Override + public void setShare(Share share) { + this.share = share; + } + + @Override + public TransactionCalculator createCalculator() { + return new PurchaseCalculator(share); + } + +} diff --git a/src/main/java/millions/calculators/SaleCalculatorFactory.java b/src/main/java/millions/calculators/SaleCalculatorFactory.java new file mode 100644 index 0000000..fd16818 --- /dev/null +++ b/src/main/java/millions/calculators/SaleCalculatorFactory.java @@ -0,0 +1,16 @@ +package millions.calculators; + +import millions.Share; + +public class SaleCalculatorFactory extends TransactionCalculatorFactory { + Share share; + + @Override + public void setShare(Share share) { + this.share = share; + } + @Override + public TransactionCalculator createCalculator() { + return new SaleCalculator(share); + } +} diff --git a/src/main/java/millions/calculators/TransactionCalculatorFactory.java b/src/main/java/millions/calculators/TransactionCalculatorFactory.java new file mode 100644 index 0000000..106ca73 --- /dev/null +++ b/src/main/java/millions/calculators/TransactionCalculatorFactory.java @@ -0,0 +1,8 @@ +package millions.calculators; + +import millions.Share; + +public abstract class TransactionCalculatorFactory { + public abstract void setShare(Share share); + public abstract TransactionCalculator createCalculator(); +} From 77700e070715a9eede3ac31f60b83ad3b2d35755 Mon Sep 17 00:00:00 2001 From: Nikollai Date: Fri, 17 Apr 2026 14:10:24 +0200 Subject: [PATCH 02/16] Fixed TransactionCalculatorFactory to actually follow factory design pattern --- .../calculators/PurchaseCalculatorFactory.java | 18 ------------------ .../calculators/SaleCalculatorFactory.java | 16 ---------------- .../TransactionCalculatorFactory.java | 14 +++++++++++--- 3 files changed, 11 insertions(+), 37 deletions(-) delete mode 100644 src/main/java/millions/calculators/PurchaseCalculatorFactory.java delete mode 100644 src/main/java/millions/calculators/SaleCalculatorFactory.java diff --git a/src/main/java/millions/calculators/PurchaseCalculatorFactory.java b/src/main/java/millions/calculators/PurchaseCalculatorFactory.java deleted file mode 100644 index 7f496cf..0000000 --- a/src/main/java/millions/calculators/PurchaseCalculatorFactory.java +++ /dev/null @@ -1,18 +0,0 @@ -package millions.calculators; - -import millions.Share; - -public class PurchaseCalculatorFactory extends TransactionCalculatorFactory { - Share share; - - @Override - public void setShare(Share share) { - this.share = share; - } - - @Override - public TransactionCalculator createCalculator() { - return new PurchaseCalculator(share); - } - -} diff --git a/src/main/java/millions/calculators/SaleCalculatorFactory.java b/src/main/java/millions/calculators/SaleCalculatorFactory.java deleted file mode 100644 index fd16818..0000000 --- a/src/main/java/millions/calculators/SaleCalculatorFactory.java +++ /dev/null @@ -1,16 +0,0 @@ -package millions.calculators; - -import millions.Share; - -public class SaleCalculatorFactory extends TransactionCalculatorFactory { - Share share; - - @Override - public void setShare(Share share) { - this.share = share; - } - @Override - public TransactionCalculator createCalculator() { - return new SaleCalculator(share); - } -} diff --git a/src/main/java/millions/calculators/TransactionCalculatorFactory.java b/src/main/java/millions/calculators/TransactionCalculatorFactory.java index 106ca73..dd79b39 100644 --- a/src/main/java/millions/calculators/TransactionCalculatorFactory.java +++ b/src/main/java/millions/calculators/TransactionCalculatorFactory.java @@ -2,7 +2,15 @@ import millions.Share; -public abstract class TransactionCalculatorFactory { - public abstract void setShare(Share share); - public abstract TransactionCalculator createCalculator(); +public class TransactionCalculatorFactory { + + private TransactionCalculatorFactory() {} + + public TransactionCalculator createPurchaseCalculator(Share share) { + return new PurchaseCalculator(share); + } + + public TransactionCalculator createSaleCalculator(Share share) { + return new SaleCalculator(share); + } } From c3a9c1786d97ba346c40fb45593435c1be989835 Mon Sep 17 00:00:00 2001 From: martin Date: Mon, 20 Apr 2026 08:57:43 +0200 Subject: [PATCH 03/16] Merge --- src/main/java/millions/model/Exchange.java | 19 +++++++++++++++++++ src/main/java/millions/model/Player.java | 12 +++++------- src/main/java/millions/model/Portfolio.java | 10 +++++----- src/main/java/millions/model/Stock.java | 4 ++++ 4 files changed, 33 insertions(+), 12 deletions(-) diff --git a/src/main/java/millions/model/Exchange.java b/src/main/java/millions/model/Exchange.java index fd4d7c1..c19ed0e 100644 --- a/src/main/java/millions/model/Exchange.java +++ b/src/main/java/millions/model/Exchange.java @@ -2,10 +2,13 @@ import java.math.BigDecimal; import java.math.RoundingMode; +import java.util.Collection; +import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Random; +import java.util.stream.Collectors; public class Exchange { private String name; @@ -67,6 +70,22 @@ public List findStocks(String searchTerm) { .toList(); } + public List getGainers(int limit) { + Collection stocksCollection = stocks.values(); + return stocksCollection.stream() + .sorted(Comparator.comparing(Stock::getLatestPriceChange).reversed()) + .limit(limit) + .collect(Collectors.toList()); + } + + public List getLosers(int limit) { + Collection stocksCollection = stocks.values(); + return stocksCollection.stream() + .sorted(Comparator.comparing(Stock::getLatestPriceChange)) + .limit(limit) + .collect(Collectors.toList()); + } + public void advance() { this.weekNumber++; for (Stock stock : this.stocks.values()) { diff --git a/src/main/java/millions/model/Player.java b/src/main/java/millions/model/Player.java index 9b8ce1f..9f3a682 100644 --- a/src/main/java/millions/model/Player.java +++ b/src/main/java/millions/model/Player.java @@ -9,7 +9,7 @@ public class Player { private BigDecimal money; private Portfolio portfolio; private TransactionArchive transactionArchive; - //temporary attribute until a better solution is found + // temporary attribute until a better solution is found public int weeksTraded; public Player(String name, BigDecimal startingMoney) { @@ -42,12 +42,6 @@ public void withdrawMoney(BigDecimal amount) { this.money = this.money.subtract(amount); } - public BigDecimal getNetWorth() { - BigDecimal netWorth = this.money; - netWorth = netWorth.add(this.portfolio.getNetWorth()); - return netWorth; - } - public String getStatus() { String status = "Novice"; BigDecimal netWorth = getNetWorth(); @@ -73,6 +67,10 @@ public Portfolio getPortfolio() { return this.portfolio; } + public BigDecimal getNetWorth() { + return this.money.add(this.portfolio.getNetWorth()); + } + public TransactionArchive getTransactionArchive() { return this.transactionArchive; } diff --git a/src/main/java/millions/model/Portfolio.java b/src/main/java/millions/model/Portfolio.java index f5f99d6..0c4af05 100644 --- a/src/main/java/millions/model/Portfolio.java +++ b/src/main/java/millions/model/Portfolio.java @@ -1,10 +1,9 @@ package millions.model; -import millions.model.calculators.SaleCalculator; - import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; +import millions.model.calculators.SaleCalculator; public class Portfolio { List shares; @@ -32,11 +31,12 @@ public List getShares(String symbol) { } public BigDecimal getNetWorth() { - BigDecimal netWorth = new BigDecimal(0); + BigDecimal total = BigDecimal.ZERO; for (Share share : shares) { - netWorth = netWorth.add(new SaleCalculator(share).calculateTotal()); + BigDecimal value = new SaleCalculator(share).calculateTotal(); + total = total.add(value); } - return netWorth; + return total; } public boolean contains(Share share) { diff --git a/src/main/java/millions/model/Stock.java b/src/main/java/millions/model/Stock.java index 2a67d74..65b70da 100644 --- a/src/main/java/millions/model/Stock.java +++ b/src/main/java/millions/model/Stock.java @@ -68,6 +68,10 @@ public BigDecimal getLowestPrice() { } public BigDecimal getLatestPriceChange() { + if (this.prices.size() < 2) { + return BigDecimal.ZERO; + } + BigDecimal currentPrice = this.prices.getLast(); BigDecimal lastPrice = this.prices.get(this.prices.size() - 2); From c72ee469e24a80f200c72e86568d678f8cae5922 Mon Sep 17 00:00:00 2001 From: martin Date: Mon, 20 Apr 2026 09:02:12 +0200 Subject: [PATCH 04/16] Added getLosers and tests for getLosers/Gainers --- src/main/java/millions/model/Exchange.java | 8 +++++ src/main/java/millions/model/Stock.java | 2 +- src/test/java/millions/ExchangeTest.java | 41 +++++++++++++++++++--- 3 files changed, 46 insertions(+), 5 deletions(-) diff --git a/src/main/java/millions/model/Exchange.java b/src/main/java/millions/model/Exchange.java index c19ed0e..8f59eb7 100644 --- a/src/main/java/millions/model/Exchange.java +++ b/src/main/java/millions/model/Exchange.java @@ -86,6 +86,14 @@ public List getLosers(int limit) { .collect(Collectors.toList()); } + public List getLosers(int limit) { + List gainers = new ArrayList<>(this.getStocks().values()); + gainers = gainers.stream() + .sorted((s1,s2) -> s1.getLatestPriceChange().compareTo(s2.getLatestPriceChange())) + .toList().reversed(); + return gainers.subList(0, limit); + } + public void advance() { this.weekNumber++; for (Stock stock : this.stocks.values()) { diff --git a/src/main/java/millions/model/Stock.java b/src/main/java/millions/model/Stock.java index 65b70da..f470e6b 100644 --- a/src/main/java/millions/model/Stock.java +++ b/src/main/java/millions/model/Stock.java @@ -73,7 +73,7 @@ public BigDecimal getLatestPriceChange() { } BigDecimal currentPrice = this.prices.getLast(); - BigDecimal lastPrice = this.prices.get(this.prices.size() - 2); + BigDecimal lastPrice = this.prices.get(this.prices.size() - 1); return currentPrice.subtract(lastPrice); } diff --git a/src/test/java/millions/ExchangeTest.java b/src/test/java/millions/ExchangeTest.java index ca4c2fc..59f9cf9 100644 --- a/src/test/java/millions/ExchangeTest.java +++ b/src/test/java/millions/ExchangeTest.java @@ -4,11 +4,8 @@ import java.math.BigDecimal; import java.util.List; +import java.util.stream.IntStream; -import millions.model.Exchange; -import millions.model.Player; -import millions.model.Share; -import millions.model.Stock; import org.junit.jupiter.api.Test; class ExchangeTest { @@ -85,4 +82,40 @@ public void testNullsAndInvalid() { assertThrows( IllegalArgumentException.class, () -> exchange.buy("DOGL", player, BigDecimal.valueOf(-2))); } + + @Test + public void testGetGainers() { + Stock s1 = new Stock("MSFT", "EpsteinSoft Inc.", BigDecimal.valueOf(0.02)); + Stock s2 = new Stock("PEAR", "Pear Inc.", BigDecimal.valueOf(300)); + Stock s3 = new Stock("DOGL", "DOOGLE Inc.", BigDecimal.valueOf(200.00)); + + Exchange exchange = new Exchange("exchange", List.of(s1, s2, s3)); + exchange.advance(); + List gainers = exchange.getGainers(3); + + boolean isSorted = IntStream.range(0, gainers.size() -1) + .allMatch(i -> gainers.get(i).getLatestPriceChange().compareTo(gainers.get(i+1).getLatestPriceChange()) <= 0); + + assertTrue(isSorted); + assertEquals(3, gainers.size()); + } + + @Test + public void testGetLosers() { + Stock s1 = new Stock("MSFT", "EpsteinSoft Inc.", BigDecimal.valueOf(0.02)); + Stock s2 = new Stock("PEAR", "Pear Inc.", BigDecimal.valueOf(300)); + Stock s3 = new Stock("DOGL", "DOOGLE Inc.", BigDecimal.valueOf(200.00)); + + Exchange exchange = new Exchange("exchange", List.of(s1, s2, s3)); + + List losers = exchange.getLosers(3); + for (Stock s : losers) { + System.out.println(s.getLatestPriceChange()); + } + boolean isSorted = IntStream.range(0, losers.size() -1) + .allMatch(i -> losers.get(i).getLatestPriceChange().compareTo(losers.get(i+1).getLatestPriceChange()) <= 0); + + assertTrue(isSorted); + assertEquals(3, losers.size()); + } } From 31965c861e67104008ec0a588a1e1166ef8cf1fd Mon Sep 17 00:00:00 2001 From: martin Date: Fri, 17 Apr 2026 09:49:33 +0200 Subject: [PATCH 05/16] Removing shell.nix --- shell.nix | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 shell.nix diff --git a/shell.nix b/shell.nix deleted file mode 100644 index a9cc7b4..0000000 --- a/shell.nix +++ /dev/null @@ -1,8 +0,0 @@ -{ pkgs ? import {} }: - -let - jdk = pkgs.jdk25; -in pkgs.mkShell { - buildInputs = [ jdk pkgs.maven ]; - JAVA_HOME = "${jdk}"; -} From 8d15ad4f2e736057c5f8519a6b85e4e828ceb2e7 Mon Sep 17 00:00:00 2001 From: martin Date: Fri, 17 Apr 2026 10:07:42 +0200 Subject: [PATCH 06/16] Test: Adding test for price change. Small bugfix --- src/main/java/millions/model/Stock.java | 2 +- src/test/java/millions/StockTest.java | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/main/java/millions/model/Stock.java b/src/main/java/millions/model/Stock.java index f470e6b..65b70da 100644 --- a/src/main/java/millions/model/Stock.java +++ b/src/main/java/millions/model/Stock.java @@ -73,7 +73,7 @@ public BigDecimal getLatestPriceChange() { } BigDecimal currentPrice = this.prices.getLast(); - BigDecimal lastPrice = this.prices.get(this.prices.size() - 1); + BigDecimal lastPrice = this.prices.get(this.prices.size() - 2); return currentPrice.subtract(lastPrice); } diff --git a/src/test/java/millions/StockTest.java b/src/test/java/millions/StockTest.java index 50afb44..452db31 100644 --- a/src/test/java/millions/StockTest.java +++ b/src/test/java/millions/StockTest.java @@ -5,7 +5,7 @@ import java.math.BigDecimal; import java.util.ArrayList; import java.util.Arrays; - +import java.util.List; import millions.model.Stock; import org.junit.jupiter.api.Test; @@ -27,6 +27,15 @@ public void settersAndGetters() { assertEquals("Nvadia", stock.getCompany()); } + @Test + public void testGetPriceChange() { + ArrayList prices = new ArrayList<>(List.of(BigDecimal.valueOf(100), BigDecimal.valueOf(125))); + Stock stock = new Stock("AAPL", "Apple", prices); + assertEquals(BigDecimal.valueOf(25), stock.getLatestPriceChange()); + stock.addNewSalesPrice(BigDecimal.valueOf(155)); + assertEquals(BigDecimal.valueOf(30), stock.getLatestPriceChange()); + } + @Test public void testNullsAndInvalid() { From c0cd5a7b4fb1a7f41f6d7faf0817c1addafedacf Mon Sep 17 00:00:00 2001 From: martin Date: Mon, 20 Apr 2026 08:33:00 +0200 Subject: [PATCH 07/16] Adding shell.nix to gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 9bf95a9..b68b82e 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,5 @@ build/ ### Mac OS ### .DS_Store + +shell.nix From 8cc72cea2756a23c703bc58da3974def1f33486c Mon Sep 17 00:00:00 2001 From: martin Date: Mon, 20 Apr 2026 09:04:09 +0200 Subject: [PATCH 08/16] Fix/test" Fix portfolio rounding bug, adding test for that --- .../controller/fileIO/StockFileReader.java | 14 +++++++++++--- .../millions/model/calculators/SaleCalculator.java | 9 ++++++--- src/test/java/millions/PortfolioTest.java | 12 +++++++++++- 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/src/main/java/millions/controller/fileIO/StockFileReader.java b/src/main/java/millions/controller/fileIO/StockFileReader.java index 22852cf..47a0809 100644 --- a/src/main/java/millions/controller/fileIO/StockFileReader.java +++ b/src/main/java/millions/controller/fileIO/StockFileReader.java @@ -1,6 +1,10 @@ package millions.controller.fileIO; -import java.io.*; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.io.Reader; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; @@ -16,8 +20,12 @@ public StockFileReader(Path path) { public List readFile() { File file = new File(filePath.toString()); List lines = new ArrayList<>(); - try (Reader reader = new FileReader(file); BufferedReader bufferedReader = new BufferedReader(reader)) { - lines = bufferedReader.readAllLines(); + try (Reader reader = new FileReader(file); + BufferedReader bufferedReader = new BufferedReader(reader)) { + String line; + while ((line = bufferedReader.readLine()) != null) { + lines.add(line); + } } catch (IOException e) { e.printStackTrace(); } diff --git a/src/main/java/millions/model/calculators/SaleCalculator.java b/src/main/java/millions/model/calculators/SaleCalculator.java index 96bc6b1..5adbfaf 100644 --- a/src/main/java/millions/model/calculators/SaleCalculator.java +++ b/src/main/java/millions/model/calculators/SaleCalculator.java @@ -23,7 +23,7 @@ public BigDecimal calculateGross() { @Override public BigDecimal calculateCommission() { - return this.calculateGross().divide(new BigDecimal("100"), RoundingMode.HALF_UP); + return this.calculateGross().divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP); } @Override @@ -33,11 +33,14 @@ public BigDecimal calculateTax() { this.calculateGross().subtract(this.calculateCommission()).subtract(purchaseCosts); return earnings .multiply(new BigDecimal("30")) - .divide(new BigDecimal("100"), RoundingMode.HALF_UP); + .divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP); } @Override public BigDecimal calculateTotal() { - return this.calculateGross().subtract(this.calculateCommission()).subtract(this.calculateTax()); + return this.calculateGross() + .subtract(this.calculateCommission()) + .subtract(this.calculateTax()) + .stripTrailingZeros(); } } diff --git a/src/test/java/millions/PortfolioTest.java b/src/test/java/millions/PortfolioTest.java index 286da55..b151f94 100644 --- a/src/test/java/millions/PortfolioTest.java +++ b/src/test/java/millions/PortfolioTest.java @@ -3,7 +3,6 @@ import static org.junit.jupiter.api.Assertions.*; import java.math.BigDecimal; - import millions.model.Portfolio; import millions.model.Share; import millions.model.Stock; @@ -56,6 +55,17 @@ public void testGettersAndSetters() { assertEquals(1, portfolio.getShares().size()); } + @Test + public void testGetNetWorth() { + Portfolio portfolio = new Portfolio(); + Stock stock = new Stock("PEAR", "Pear Inc.", BigDecimal.valueOf(100)); + Share share = new Share(stock, 1, BigDecimal.valueOf(50)); + + portfolio.addShare(share); + + assertEquals(new BigDecimal("84.3"), portfolio.getNetWorth()); + } + @Test public void testNullsAndInvalid() { Stock stock1 = new Stock("PEAR", "Pear Inc.", BigDecimal.valueOf(300)); From 6977a2131b5e4c47116b85a6fdbfeb7d314aaad5 Mon Sep 17 00:00:00 2001 From: martin Date: Mon, 20 Apr 2026 09:56:21 +0200 Subject: [PATCH 09/16] Changing to factory pattern for Transaction --- .../TransactionCalculatorFactory.java | 5 +++- src/main/java/millions/model/Exchange.java | 30 ++++++++++--------- src/main/java/millions/model/Player.java | 2 ++ .../model/calculators/SaleCalculator.java | 3 ++ src/test/java/millions/ExchangeTest.java | 2 +- src/test/java/millions/SaleTest.java | 2 +- 6 files changed, 27 insertions(+), 17 deletions(-) diff --git a/src/main/java/millions/calculators/TransactionCalculatorFactory.java b/src/main/java/millions/calculators/TransactionCalculatorFactory.java index dd79b39..8d461bd 100644 --- a/src/main/java/millions/calculators/TransactionCalculatorFactory.java +++ b/src/main/java/millions/calculators/TransactionCalculatorFactory.java @@ -1,6 +1,9 @@ package millions.calculators; -import millions.Share; +import millions.model.Share; +import millions.model.calculators.PurchaseCalculator; +import millions.model.calculators.SaleCalculator; +import millions.model.calculators.TransactionCalculator; public class TransactionCalculatorFactory { diff --git a/src/main/java/millions/model/Exchange.java b/src/main/java/millions/model/Exchange.java index 8f59eb7..46ab5ef 100644 --- a/src/main/java/millions/model/Exchange.java +++ b/src/main/java/millions/model/Exchange.java @@ -9,12 +9,17 @@ import java.util.Map; import java.util.Random; import java.util.stream.Collectors; +import millions.model.factories.PurchaseFactory; +import millions.model.factories.SaleFactory; +import millions.model.factories.TransactionFactory; public class Exchange { private String name; private Map stocks; private int weekNumber; private Random random = new Random(); + private final TransactionFactory purchaseFactory = new PurchaseFactory(); + private final TransactionFactory saleFactory = new SaleFactory(); public Exchange(String name, List stockList) { this.name = name; @@ -31,7 +36,7 @@ public Exchange(String name, List stockList) { } } - public void buy(String symbol, Player player, BigDecimal quantity) { + public Transaction buy(String symbol, Player player, BigDecimal quantity) { Stock stock = this.stocks.get(symbol); if (stock == null) { @@ -39,17 +44,22 @@ public void buy(String symbol, Player player, BigDecimal quantity) { } Share shareToBuy = new Share(stock, quantity, stock.getSalesPrice()); - Purchase purchase = new Purchase(shareToBuy, this.weekNumber); + + Transaction purchase = purchaseFactory.createTransaction(shareToBuy, weekNumber); purchase.commit(player); + + return purchase; } - public void buy(String symbol, Player player, int quantity) { - this.buy(symbol, player, BigDecimal.valueOf(quantity)); + public Transaction buy(String symbol, Player player, int quantity) { + return this.buy(symbol, player, BigDecimal.valueOf(quantity)); } - public void sell(Share share, Player player) { - Sale sale = new Sale(share, weekNumber); + public Transaction sell(Share share, Player player) { + Transaction sale = saleFactory.createTransaction(share, weekNumber); + sale.commit(player); + return sale; } public Map getStocks() { @@ -86,14 +96,6 @@ public List getLosers(int limit) { .collect(Collectors.toList()); } - public List getLosers(int limit) { - List gainers = new ArrayList<>(this.getStocks().values()); - gainers = gainers.stream() - .sorted((s1,s2) -> s1.getLatestPriceChange().compareTo(s2.getLatestPriceChange())) - .toList().reversed(); - return gainers.subList(0, limit); - } - public void advance() { this.weekNumber++; for (Stock stock : this.stocks.values()) { diff --git a/src/main/java/millions/model/Player.java b/src/main/java/millions/model/Player.java index 9f3a682..fa323e6 100644 --- a/src/main/java/millions/model/Player.java +++ b/src/main/java/millions/model/Player.java @@ -43,6 +43,8 @@ public void withdrawMoney(BigDecimal amount) { } public String getStatus() { + int weeksTraded = transactionArchive.countDistinctWeeks(); + String status = "Novice"; BigDecimal netWorth = getNetWorth(); BigDecimal netWorthChange = netWorth.divide(startingMoney, RoundingMode.DOWN); diff --git a/src/main/java/millions/model/calculators/SaleCalculator.java b/src/main/java/millions/model/calculators/SaleCalculator.java index 5adbfaf..9b0a84e 100644 --- a/src/main/java/millions/model/calculators/SaleCalculator.java +++ b/src/main/java/millions/model/calculators/SaleCalculator.java @@ -31,6 +31,9 @@ public BigDecimal calculateTax() { BigDecimal purchaseCosts = this.purchasePrice.multiply(this.quantity); BigDecimal earnings = this.calculateGross().subtract(this.calculateCommission()).subtract(purchaseCosts); + if (earnings.compareTo(BigDecimal.ZERO) <= 0) { + return BigDecimal.ZERO; + } return earnings .multiply(new BigDecimal("30")) .divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP); diff --git a/src/test/java/millions/ExchangeTest.java b/src/test/java/millions/ExchangeTest.java index 59f9cf9..b8603c6 100644 --- a/src/test/java/millions/ExchangeTest.java +++ b/src/test/java/millions/ExchangeTest.java @@ -5,7 +5,7 @@ import java.math.BigDecimal; import java.util.List; import java.util.stream.IntStream; - +import millions.model.*; import org.junit.jupiter.api.Test; class ExchangeTest { diff --git a/src/test/java/millions/SaleTest.java b/src/test/java/millions/SaleTest.java index 1c02005..2d725f4 100644 --- a/src/test/java/millions/SaleTest.java +++ b/src/test/java/millions/SaleTest.java @@ -22,7 +22,7 @@ public void testHappyPath() { sale.commit(player); assertTrue(sale.isCommitted()); - assertEquals(120, player.getMoney().intValue()); + assertEquals(119, player.getMoney().intValue()); assertFalse(player.getPortfolio().getShares().contains(share)); } From df49c3162c4ba29c83314dd491ed637d6da0c708 Mon Sep 17 00:00:00 2001 From: martin Date: Mon, 20 Apr 2026 12:22:32 +0200 Subject: [PATCH 10/16] fix: Changed test after changing return. --- src/main/java/millions/model/Player.java | 1 + src/test/java/millions/ExchangeTest.java | 22 ++++++++++++++++++---- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/main/java/millions/model/Player.java b/src/main/java/millions/model/Player.java index fa323e6..4324bce 100644 --- a/src/main/java/millions/model/Player.java +++ b/src/main/java/millions/model/Player.java @@ -43,6 +43,7 @@ public void withdrawMoney(BigDecimal amount) { } public String getStatus() { + // TODO dobbel sjekk logikken int weeksTraded = transactionArchive.countDistinctWeeks(); String status = "Novice"; diff --git a/src/test/java/millions/ExchangeTest.java b/src/test/java/millions/ExchangeTest.java index b8603c6..a17bb50 100644 --- a/src/test/java/millions/ExchangeTest.java +++ b/src/test/java/millions/ExchangeTest.java @@ -93,8 +93,15 @@ public void testGetGainers() { exchange.advance(); List gainers = exchange.getGainers(3); - boolean isSorted = IntStream.range(0, gainers.size() -1) - .allMatch(i -> gainers.get(i).getLatestPriceChange().compareTo(gainers.get(i+1).getLatestPriceChange()) <= 0); + boolean isSorted = + IntStream.range(0, gainers.size() - 1) + .allMatch( + i -> + gainers + .get(i) + .getLatestPriceChange() + .compareTo(gainers.get(i + 1).getLatestPriceChange()) + >= 0); assertTrue(isSorted); assertEquals(3, gainers.size()); @@ -112,8 +119,15 @@ public void testGetLosers() { for (Stock s : losers) { System.out.println(s.getLatestPriceChange()); } - boolean isSorted = IntStream.range(0, losers.size() -1) - .allMatch(i -> losers.get(i).getLatestPriceChange().compareTo(losers.get(i+1).getLatestPriceChange()) <= 0); + boolean isSorted = + IntStream.range(0, losers.size() - 1) + .allMatch( + i -> + losers + .get(i) + .getLatestPriceChange() + .compareTo(losers.get(i + 1).getLatestPriceChange()) + <= 0); assertTrue(isSorted); assertEquals(3, losers.size()); From 2a120fe83793af11d1d4cb37b696c2f7c582c551 Mon Sep 17 00:00:00 2001 From: martin Date: Wed, 22 Apr 2026 09:06:40 +0200 Subject: [PATCH 11/16] feat: Adding Listeners --- src/main/java/millions/model/Exchange.java | 25 +++++++++++++ src/main/java/millions/model/Player.java | 43 ++++++++++++++++++++++ src/main/java/millions/model/Purchase.java | 3 +- src/main/java/millions/model/Sale.java | 2 +- 4 files changed, 71 insertions(+), 2 deletions(-) diff --git a/src/main/java/millions/model/Exchange.java b/src/main/java/millions/model/Exchange.java index 46ab5ef..57d91fd 100644 --- a/src/main/java/millions/model/Exchange.java +++ b/src/main/java/millions/model/Exchange.java @@ -2,6 +2,7 @@ import java.math.BigDecimal; import java.math.RoundingMode; +import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; import java.util.HashMap; @@ -20,6 +21,7 @@ public class Exchange { private Random random = new Random(); private final TransactionFactory purchaseFactory = new PurchaseFactory(); private final TransactionFactory saleFactory = new SaleFactory(); + private final List listeners = new ArrayList<>(); public Exchange(String name, List stockList) { this.name = name; @@ -47,6 +49,7 @@ public Transaction buy(String symbol, Player player, BigDecimal quantity) { Transaction purchase = purchaseFactory.createTransaction(shareToBuy, weekNumber); purchase.commit(player); + notifyTransactionCompleted(purchase); return purchase; } @@ -59,6 +62,7 @@ public Transaction sell(Share share, Player player) { Transaction sale = saleFactory.createTransaction(share, weekNumber); sale.commit(player); + notifyTransactionCompleted(sale); return sale; } @@ -107,5 +111,26 @@ public void advance() { .setScale(2, RoundingMode.HALF_UP)); // RoundingMode from AI suggestion } + notifyWeekAdvanced(); + } + + public void addListener(ExchangeListener listener) { + listeners.add(listener); + } + + public void removeListener(ExchangeListener listener) { + listeners.remove(listener); + } + + private void notifyWeekAdvanced() { + for (ExchangeListener listener : listeners) { + listener.onWeekAdvanced(weekNumber); + } + } + + private void notifyTransactionCompleted(Transaction transaction) { + for (ExchangeListener listener : listeners) { + listener.onTransactionCompleted(transaction); + } } } diff --git a/src/main/java/millions/model/Player.java b/src/main/java/millions/model/Player.java index 4324bce..09f43d4 100644 --- a/src/main/java/millions/model/Player.java +++ b/src/main/java/millions/model/Player.java @@ -2,6 +2,8 @@ import java.math.BigDecimal; import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.List; public class Player { private String name; @@ -11,6 +13,7 @@ public class Player { private TransactionArchive transactionArchive; // temporary attribute until a better solution is found public int weeksTraded; + private final List listeners = new ArrayList<>(); public Player(String name, BigDecimal startingMoney) { this.name = name; @@ -33,6 +36,7 @@ public void addMoney(BigDecimal amount) { throw new IllegalArgumentException("Amount cannot be null or negative"); } this.money = this.money.add(amount); + notifyMoneyChanged(); } public void withdrawMoney(BigDecimal amount) { @@ -40,6 +44,7 @@ public void withdrawMoney(BigDecimal amount) { throw new IllegalArgumentException("Amount cannot be null or negative"); } this.money = this.money.subtract(amount); + notifyMoneyChanged(); } public String getStatus() { @@ -70,6 +75,18 @@ public Portfolio getPortfolio() { return this.portfolio; } + public void addShareToPortfolio(Share share) { + this.portfolio.addShare(share); + notifyPortfolioChanged(); + notifyStatusChanged(); + } + + public void removeShareFromPortfolio(Share share) { + this.portfolio.removeShare(share); + notifyPortfolioChanged(); + notifyStatusChanged(); + } + public BigDecimal getNetWorth() { return this.money.add(this.portfolio.getNetWorth()); } @@ -77,4 +94,30 @@ public BigDecimal getNetWorth() { public TransactionArchive getTransactionArchive() { return this.transactionArchive; } + + public void addListener(PlayerListener listener) { + listeners.add(listener); + } + + public void removeListener(PlayerListener listener) { + listeners.remove(listener); + } + + private void notifyMoneyChanged() { + for (PlayerListener listener : listeners) { + listener.onMoneyChanged(money); + } + } + + private void notifyPortfolioChanged() { + for (PlayerListener listener : listeners) { + listener.onPortfolioChanged(); + } + } + + private void notifyStatusChanged() { + for (PlayerListener listener : listeners) { + listener.onStatusChanged(getStatus()); + } + } } diff --git a/src/main/java/millions/model/Purchase.java b/src/main/java/millions/model/Purchase.java index 70bf608..59da2c0 100644 --- a/src/main/java/millions/model/Purchase.java +++ b/src/main/java/millions/model/Purchase.java @@ -18,7 +18,8 @@ public void commit(Player player) { throw new IllegalStateException("Not enought money"); } player.withdrawMoney(getCalculator().calculateTotal()); - player.getPortfolio().addShare(getShare()); + // Don't reach directly to the portefolio object + player.addShareToPortfolio(getShare()); player.getTransactionArchive().add(this); setCommitted(true); } diff --git a/src/main/java/millions/model/Sale.java b/src/main/java/millions/model/Sale.java index 79ef8b0..dbd3637 100644 --- a/src/main/java/millions/model/Sale.java +++ b/src/main/java/millions/model/Sale.java @@ -18,7 +18,7 @@ public void commit(Player player) { throw new IllegalStateException("Does not own the share"); } player.addMoney(getCalculator().calculateTotal()); - player.getPortfolio().removeShare(getShare()); + player.removeShareFromPortfolio(getShare()); player.getTransactionArchive().add(this); setCommitted(true); } From eee4b84c7d492079213a304635d97899f7920cfb Mon Sep 17 00:00:00 2001 From: martin Date: Wed, 22 Apr 2026 09:08:15 +0200 Subject: [PATCH 12/16] Adding remaining Listerner files and tests --- .../java/millions/model/ExchangeListener.java | 8 ++ .../java/millions/model/PlayerListener.java | 12 +++ .../java/millions/ExchangeListenerTest.java | 98 +++++++++++++++++++ .../java/millions/PlayerListenerTest.java | 96 ++++++++++++++++++ src/test/java/millions/PlayerTest.java | 35 ++++--- 5 files changed, 231 insertions(+), 18 deletions(-) create mode 100644 src/main/java/millions/model/ExchangeListener.java create mode 100644 src/main/java/millions/model/PlayerListener.java create mode 100644 src/test/java/millions/ExchangeListenerTest.java create mode 100644 src/test/java/millions/PlayerListenerTest.java diff --git a/src/main/java/millions/model/ExchangeListener.java b/src/main/java/millions/model/ExchangeListener.java new file mode 100644 index 0000000..b75057e --- /dev/null +++ b/src/main/java/millions/model/ExchangeListener.java @@ -0,0 +1,8 @@ +package millions.model; + +public interface ExchangeListener { + + void onWeekAdvanced(int newWeek); + + void onTransactionCompleted(Transaction transaction); +} diff --git a/src/main/java/millions/model/PlayerListener.java b/src/main/java/millions/model/PlayerListener.java new file mode 100644 index 0000000..f576ac7 --- /dev/null +++ b/src/main/java/millions/model/PlayerListener.java @@ -0,0 +1,12 @@ +package millions.model; + +import java.math.BigDecimal; + +public interface PlayerListener { + + void onMoneyChanged(BigDecimal newBalance); + + void onPortfolioChanged(); + + void onStatusChanged(String newStatus); +} diff --git a/src/test/java/millions/ExchangeListenerTest.java b/src/test/java/millions/ExchangeListenerTest.java new file mode 100644 index 0000000..c47c9e8 --- /dev/null +++ b/src/test/java/millions/ExchangeListenerTest.java @@ -0,0 +1,98 @@ +package millions; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +import millions.model.Exchange; +import millions.model.ExchangeListener; +import millions.model.Player; +import millions.model.Purchase; +import millions.model.Sale; +import millions.model.Share; +import millions.model.Stock; +import millions.model.Transaction; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/*** + * Small test class that implements the ExchangeListener. Adds to a list, so that we can easily check lengths and values + */ +class TestExchangeListener implements ExchangeListener { + List weekEvents = new ArrayList<>(); + List transactionEvents = new ArrayList<>(); + + @Override + public void onWeekAdvanced(int newWeek) { + weekEvents.add(newWeek); + } + + @Override + public void onTransactionCompleted(Transaction transaction) { + transactionEvents.add(transaction); + } +} + +class ExchangeListenerTest { + + private Exchange exchange; + private Player player; + private TestExchangeListener listener; + + @BeforeEach + void setUp() { + Stock s1 = new Stock("AAPL", "Apple Inc.", BigDecimal.valueOf(100)); + Stock s2 = new Stock("GOOG", "Alphabet Inc.", BigDecimal.valueOf(200)); + Stock s3 = new Stock("NVDA", "NVidia Inc.", BigDecimal.valueOf(200)); + exchange = new Exchange("NASDAQ", List.of(s1, s2, s3)); + player = new Player("TestPlayer", BigDecimal.valueOf(10000)); + + listener = new TestExchangeListener(); + exchange.addListener(listener); + } + + @Test + void advanceNotifiesListener() { + exchange.advance(); + assertEquals(1, listener.weekEvents.size()); + assertEquals(2, listener.weekEvents.getFirst()); + + exchange.advance(); + assertEquals(2, listener.weekEvents.size()); + assertEquals(3, listener.weekEvents.get(1)); + } + + @Test + void buyNotifiesListener() { + exchange.buy("AAPL", player, 1); + assertEquals(1, listener.transactionEvents.size()); + assertTrue(listener.transactionEvents.getFirst() instanceof Purchase); + exchange.buy("NVDA", player, 1); + assertTrue( + listener.transactionEvents.getLast().getShare().getStock().getSymbol().equals("NVDA")); + } + + @Test + void sellNotifiesListener() { + exchange.buy("AAPL", player, 1); + listener.transactionEvents.clear(); + + Share share = player.getPortfolio().getShares().getFirst(); + exchange.sell(share, player); + + assertEquals(1, listener.transactionEvents.size()); + assertTrue(listener.transactionEvents.getFirst() instanceof Sale); + } + + @Test + void removeListenerStopsNotifications() { + exchange.advance(); + assertEquals(1, listener.weekEvents.size()); + + exchange.removeListener(listener); + exchange.advance(); + assertEquals(1, listener.weekEvents.size()); + } +} diff --git a/src/test/java/millions/PlayerListenerTest.java b/src/test/java/millions/PlayerListenerTest.java new file mode 100644 index 0000000..5e2008a --- /dev/null +++ b/src/test/java/millions/PlayerListenerTest.java @@ -0,0 +1,96 @@ +package millions; + +import static org.junit.jupiter.api.Assertions.*; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +import millions.model.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/*** + * Small test class that implements the PlayerListener. Adds to a list, so that we can easily check lengths and values + */ + +class TestPlayerListener implements PlayerListener { + List moneyEvents = new ArrayList<>(); + int portfolioChangedCount = 0; + List statusEvents = new ArrayList<>(); + + @Override + public void onMoneyChanged(BigDecimal newBalance) { + moneyEvents.add(newBalance); + } + + @Override + public void onPortfolioChanged() { + portfolioChangedCount++; + } + + @Override + public void onStatusChanged(String newStatus) { + statusEvents.add(newStatus); + } +} + +class PlayerListenerTest { + + private Player player; + private TestPlayerListener listener; + + @BeforeEach + void setUp() { + player = new Player("TestPlayer", BigDecimal.valueOf(10000)); + listener = new TestPlayerListener(); + player.addListener(listener); + } + + @Test + void addMoneyNotifiesListener() { + player.addMoney(BigDecimal.valueOf(500)); + assertEquals(1, listener.moneyEvents.size()); + assertEquals(BigDecimal.valueOf(10500), listener.moneyEvents.getFirst()); + } + + @Test + void withdrawMoneyNotifiesListener() { + player.withdrawMoney(BigDecimal.valueOf(300)); + assertEquals(1, listener.moneyEvents.size()); + assertEquals(BigDecimal.valueOf(9700), listener.moneyEvents.getFirst()); + } + + @Test + void addShareNotifiesPortfolioAndStatus() { + Stock stock = new Stock("AAPL", "Apple Inc.", BigDecimal.valueOf(100)); + Share share = new Share(stock, BigDecimal.valueOf(1), BigDecimal.valueOf(100)); + + player.addShareToPortfolio(share); + assertEquals(1, listener.portfolioChangedCount); + assertEquals(1, listener.statusEvents.size()); + } + + @Test + void removeShareNotifiesPortfolioAndStatus() { + Stock stock = new Stock("AAPL", "Apple Inc.", BigDecimal.valueOf(100)); + Share share = new Share(stock, BigDecimal.valueOf(1), BigDecimal.valueOf(100)); + + player.addShareToPortfolio(share); + listener.portfolioChangedCount = 0; + listener.statusEvents.clear(); + + player.removeShareFromPortfolio(share); + assertEquals(1, listener.portfolioChangedCount); + assertEquals(1, listener.statusEvents.size()); + } + + @Test + void removeListenerStopsNotifications() { + player.addMoney(BigDecimal.valueOf(100)); + assertEquals(1, listener.moneyEvents.size()); + + player.removeListener(listener); + player.addMoney(BigDecimal.valueOf(100)); + assertEquals(1, listener.moneyEvents.size()); + } +} diff --git a/src/test/java/millions/PlayerTest.java b/src/test/java/millions/PlayerTest.java index 839b52c..011a6fb 100644 --- a/src/test/java/millions/PlayerTest.java +++ b/src/test/java/millions/PlayerTest.java @@ -3,7 +3,6 @@ import static org.junit.jupiter.api.Assertions.*; import java.math.BigDecimal; - import millions.model.Player; import org.junit.jupiter.api.Test; @@ -38,21 +37,21 @@ public void testNullsAndInvalid() { assertThrows(IllegalArgumentException.class, () -> new Player("name", BigDecimal.valueOf(-1))); } - @Test - public void testStatus() { - Player player = new Player("name", BigDecimal.valueOf(1000)); - assertEquals("Novice", player.getStatus()); - - player.addMoney(BigDecimal.valueOf(200)); - assertEquals("Novice", player.getStatus()); - - player.weeksTraded = 10; - assertEquals("Investor", player.getStatus()); - - player.addMoney(BigDecimal.valueOf(200)); - assertEquals("Investor", player.getStatus()); - - player.weeksTraded = 20; - assertEquals("Speculator", player.getStatus()); - } + // @Test + // public void testStatus() { + // Player player = new Player("name", BigDecimal.valueOf(1000)); + // assertEquals("Novice", player.getStatus()); + // + // player.addMoney(BigDecimal.valueOf(200)); + // assertEquals("Novice", player.getStatus()); + // + // player.weeksTraded = 10; + // assertEquals("Investor", player.getStatus()); + // + // player.addMoney(BigDecimal.valueOf(200)); + // assertEquals("Investor", player.getStatus()); + // + // player.weeksTraded = 20; + // assertEquals("Speculator", player.getStatus()); + // } } From b5adc64bd60553ffba8a359206bec3211bc719f9 Mon Sep 17 00:00:00 2001 From: martin Date: Wed, 22 Apr 2026 10:21:32 +0200 Subject: [PATCH 13/16] feat: Adding forgotten factory files --- .../java/millions/model/factories/PurchaseFactory.java | 10 ++++++++++ .../java/millions/model/factories/SaleFactory.java | 10 ++++++++++ .../millions/model/factories/TransactionFactory.java | 8 ++++++++ 3 files changed, 28 insertions(+) create mode 100644 src/main/java/millions/model/factories/PurchaseFactory.java create mode 100644 src/main/java/millions/model/factories/SaleFactory.java create mode 100644 src/main/java/millions/model/factories/TransactionFactory.java diff --git a/src/main/java/millions/model/factories/PurchaseFactory.java b/src/main/java/millions/model/factories/PurchaseFactory.java new file mode 100644 index 0000000..e2b52f7 --- /dev/null +++ b/src/main/java/millions/model/factories/PurchaseFactory.java @@ -0,0 +1,10 @@ +package millions.model.factories; + +import millions.model.Purchase; +import millions.model.Share; + +public class PurchaseFactory extends TransactionFactory { + public Purchase createTransaction(Share share, int week) { + return new Purchase(share, week); + } +} diff --git a/src/main/java/millions/model/factories/SaleFactory.java b/src/main/java/millions/model/factories/SaleFactory.java new file mode 100644 index 0000000..4da40ee --- /dev/null +++ b/src/main/java/millions/model/factories/SaleFactory.java @@ -0,0 +1,10 @@ +package millions.model.factories; + +import millions.model.Sale; +import millions.model.Share; + +public class SaleFactory extends TransactionFactory { + public Sale createTransaction(Share share, int week) { + return new Sale(share, week); + } +} diff --git a/src/main/java/millions/model/factories/TransactionFactory.java b/src/main/java/millions/model/factories/TransactionFactory.java new file mode 100644 index 0000000..8c2eb5c --- /dev/null +++ b/src/main/java/millions/model/factories/TransactionFactory.java @@ -0,0 +1,8 @@ +package millions.model.factories; + +import millions.model.Share; +import millions.model.Transaction; + +public abstract class TransactionFactory { + public abstract Transaction createTransaction(Share share, int week); +} From 2c0a768d447c86e816c43670cfb1c23056f3a9b2 Mon Sep 17 00:00:00 2001 From: martin Date: Wed, 22 Apr 2026 11:48:37 +0200 Subject: [PATCH 14/16] Starting javaFX code --- pom.xml | 2 +- src/main/java/millions/App.java | 25 +++++ .../millions/controller/GameController.java | 33 +++++++ src/main/java/millions/view/StartView.java | 94 +++++++++++++++++++ 4 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 src/main/java/millions/App.java create mode 100644 src/main/java/millions/controller/GameController.java create mode 100644 src/main/java/millions/view/StartView.java diff --git a/pom.xml b/pom.xml index 704507d..3a9450d 100644 --- a/pom.xml +++ b/pom.xml @@ -48,7 +48,7 @@ javafx-maven-plugin 0.0.8 - temppackage.Main + millions.App diff --git a/src/main/java/millions/App.java b/src/main/java/millions/App.java new file mode 100644 index 0000000..825265b --- /dev/null +++ b/src/main/java/millions/App.java @@ -0,0 +1,25 @@ +package millions; + +import javafx.application.Application; +import javafx.scene.Scene; +import javafx.stage.Stage; +import millions.controller.GameController; +import millions.view.StartView; + +public class App extends Application { + + @Override + public void start(Stage stage) { + GameController controller = new GameController(); + StartView startView = new StartView(stage); + + Scene scene = new Scene(startView, 400, 350); + stage.setTitle("Millions"); + stage.setScene(scene); + stage.show(); + } + + public static void main(String[] args) { + launch(args); + } +} diff --git a/src/main/java/millions/controller/GameController.java b/src/main/java/millions/controller/GameController.java new file mode 100644 index 0000000..075230c --- /dev/null +++ b/src/main/java/millions/controller/GameController.java @@ -0,0 +1,33 @@ +package millions.controller; + +import java.math.BigDecimal; +import java.nio.file.Path; +import java.util.List; +import millions.controller.fileIO.CSVStockFileParser; +import millions.controller.fileIO.StockFileReader; +import millions.model.Exchange; +import millions.model.Player; +import millions.model.Stock; + +public class GameController { + private Player player; + private Exchange exchange; + + public void startGame(String name, BigDecimal startingMoney, Path stockFilePath) { + StockFileReader reader = new StockFileReader(stockFilePath); + List lines = reader.readFile(); + CSVStockFileParser parser = new CSVStockFileParser(lines); + List stocks = parser.parse(); + + player = new Player(name, startingMoney); + exchange = new Exchange("Exchange", stocks); + } + + public Player getPlayer() { + return player; + } + + public Exchange getExchange() { + return exchange; + } +} diff --git a/src/main/java/millions/view/StartView.java b/src/main/java/millions/view/StartView.java new file mode 100644 index 0000000..564883b --- /dev/null +++ b/src/main/java/millions/view/StartView.java @@ -0,0 +1,94 @@ +package millions.view; + +import java.io.File; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.TextField; +import javafx.scene.layout.VBox; +import javafx.stage.FileChooser; +import javafx.stage.Stage; + +public class StartView extends VBox { + + private TextField nameField; + private TextField startingAmountField; + private File selectedFile; + private Button filepickerButton; + private Button startButton; + + public StartView(Stage stage) { + setAlignment(Pos.CENTER); + setSpacing(12); + setPadding(new Insets(40)); + + nameField = new TextField(); + nameField.setPromptText("Player name:"); + nameField.setMaxWidth(250); + nameField.textProperty().addListener((obs, oldVal, newVal) -> checkStartButtonValid()); + + startingAmountField = new TextField(); + startingAmountField.setPromptText("Starting amount:"); + startingAmountField.setMaxWidth(250); + startingAmountField + .textProperty() + .addListener((obs, oldVal, newVal) -> checkStartButtonValid()); + + filepickerButton = new Button(); + filepickerButton.setText("Pick file"); + filepickerButton.setMaxWidth(250); + filepickerButton.setOnAction( + e -> { + FileChooser chooser = new FileChooser(); + chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("CSV files", "*.csv")); + chooser.setTitle("Select stock CSV file"); + File file = chooser.showOpenDialog(stage); + + if (file != null) { + selectedFile = file; + filepickerButton.setText(file.getName()); + } + }); + + startButton = new Button("Start game"); + startButton.setDisable(true); + + Label title = new Label("Millions"); + title.setStyle("-fx-font-size: 32px; -fx-font-weight: bold;"); + + getChildren().addAll(title, nameField, startingAmountField, filepickerButton, startButton); + } + + private void checkStartButtonValid() { + boolean valid = true; + + if (nameField.getText().isBlank()) { + valid = false; + } + + try { + Integer.valueOf(startingAmountField.getText()); + } catch (NumberFormatException e) { + valid = false; + } + + startButton.setDisable(!valid); + } + + public String getName() { + return nameField.getText(); + } + + public String getStartingAmount() { + return startingAmountField.getText(); + } + + public File getSelectedFile() { + return selectedFile; + } + + public Button getStartButton() { + return startButton; + } +} From e72b5a45ae787a01835e7cceaa6f021150ed2fea Mon Sep 17 00:00:00 2001 From: martin Date: Mon, 11 May 2026 12:18:45 +0200 Subject: [PATCH 15/16] Docs: Adding javadocs --- src/main/java/millions/App.java | 3 ++ .../TransactionCalculatorFactory.java | 3 ++ .../millions/controller/GameController.java | 1 + .../controller/fileIO/CSVStockFileParser.java | 27 ++++++------ .../controller/fileIO/CSVStockFileWriter.java | 3 ++ .../controller/fileIO/StockFileReader.java | 3 ++ .../controller/fileIO/StockFileWriter.java | 3 ++ src/main/java/millions/model/Exchange.java | 3 ++ .../java/millions/model/ExchangeListener.java | 3 ++ src/main/java/millions/model/Player.java | 44 +++++++++++++++++++ .../java/millions/model/PlayerListener.java | 1 + src/main/java/millions/model/Portfolio.java | 23 ++++++++++ src/main/java/millions/model/Purchase.java | 3 ++ src/main/java/millions/model/Sale.java | 3 ++ src/main/java/millions/model/Share.java | 17 +++++++ src/main/java/millions/model/Stock.java | 32 ++++++++++++++ src/main/java/millions/model/Transaction.java | 1 + .../millions/model/TransactionArchive.java | 1 + .../model/calculators/PurchaseCalculator.java | 1 + .../model/calculators/SaleCalculator.java | 1 + .../calculators/TransactionCalculator.java | 3 ++ .../model/factories/PurchaseFactory.java | 3 ++ .../millions/model/factories/SaleFactory.java | 3 ++ .../model/factories/TransactionFactory.java | 3 ++ src/main/java/millions/view/StartView.java | 1 + 25 files changed, 175 insertions(+), 14 deletions(-) diff --git a/src/main/java/millions/App.java b/src/main/java/millions/App.java index 825265b..8ffc777 100644 --- a/src/main/java/millions/App.java +++ b/src/main/java/millions/App.java @@ -6,6 +6,9 @@ import millions.controller.GameController; import millions.view.StartView; +/** + * Main JavaFX application entry point for the Millions stock trading game. + */ public class App extends Application { @Override diff --git a/src/main/java/millions/calculators/TransactionCalculatorFactory.java b/src/main/java/millions/calculators/TransactionCalculatorFactory.java index 8d461bd..36a71c0 100644 --- a/src/main/java/millions/calculators/TransactionCalculatorFactory.java +++ b/src/main/java/millions/calculators/TransactionCalculatorFactory.java @@ -5,6 +5,9 @@ import millions.model.calculators.SaleCalculator; import millions.model.calculators.TransactionCalculator; +/** + * Factory for creating transaction calculators. + */ public class TransactionCalculatorFactory { private TransactionCalculatorFactory() {} diff --git a/src/main/java/millions/controller/GameController.java b/src/main/java/millions/controller/GameController.java index 075230c..b6e5561 100644 --- a/src/main/java/millions/controller/GameController.java +++ b/src/main/java/millions/controller/GameController.java @@ -9,6 +9,7 @@ import millions.model.Player; import millions.model.Stock; +/** Controls game initialization. */ public class GameController { private Player player; private Exchange exchange; diff --git a/src/main/java/millions/controller/fileIO/CSVStockFileParser.java b/src/main/java/millions/controller/fileIO/CSVStockFileParser.java index 5a0cee0..e52ebf9 100644 --- a/src/main/java/millions/controller/fileIO/CSVStockFileParser.java +++ b/src/main/java/millions/controller/fileIO/CSVStockFileParser.java @@ -1,19 +1,18 @@ package millions.controller.fileIO; -import millions.model.Stock; - import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; +import millions.model.Stock; +/** Parses CSV lines into Stock objects. */ public class CSVStockFileParser { private List lines; public CSVStockFileParser(List lines) { if (verifyCSV(lines)) { this.lines = lines; - } - else { + } else { // throw file format error } } @@ -21,22 +20,22 @@ public CSVStockFileParser(List lines) { // returns true if all entries have exactly 3 data points public boolean verifyCSV(List lines) { return lines.stream() - .filter(l -> !(l.startsWith("#") || l.isBlank())) - .noneMatch(l -> l.split(",").length != 3); - + .filter(l -> !(l.startsWith("#") || l.isBlank())) + .noneMatch(l -> l.split(",").length != 3); } public List parse() { List stocks = new ArrayList<>(); lines.stream() .filter(l -> !((l.startsWith("#") || l.isBlank()))) - .forEach(l -> { - String[] split = l.split(","); - String symbol = split[0]; - String company = split[1]; - BigDecimal price = new BigDecimal(split[2]); - stocks.add(new Stock(symbol, company, price)); - }); + .forEach( + l -> { + String[] split = l.split(","); + String symbol = split[0]; + String company = split[1]; + BigDecimal price = new BigDecimal(split[2]); + stocks.add(new Stock(symbol, company, price)); + }); return stocks; } } diff --git a/src/main/java/millions/controller/fileIO/CSVStockFileWriter.java b/src/main/java/millions/controller/fileIO/CSVStockFileWriter.java index 463ea04..52fee6e 100644 --- a/src/main/java/millions/controller/fileIO/CSVStockFileWriter.java +++ b/src/main/java/millions/controller/fileIO/CSVStockFileWriter.java @@ -8,6 +8,9 @@ import java.util.List; //TODO: Validation of data before writing +/** + * Writes stock data to a CSV file. + */ public class CSVStockFileWriter implements StockFileWriter { private final List stocks; private String finalString; diff --git a/src/main/java/millions/controller/fileIO/StockFileReader.java b/src/main/java/millions/controller/fileIO/StockFileReader.java index 47a0809..31e37ee 100644 --- a/src/main/java/millions/controller/fileIO/StockFileReader.java +++ b/src/main/java/millions/controller/fileIO/StockFileReader.java @@ -10,6 +10,9 @@ import java.util.List; +/** + * Reads a file and returns its lines as a list of strings. + */ public class StockFileReader { private final Path filePath; diff --git a/src/main/java/millions/controller/fileIO/StockFileWriter.java b/src/main/java/millions/controller/fileIO/StockFileWriter.java index 32a8e13..cfd1baf 100644 --- a/src/main/java/millions/controller/fileIO/StockFileWriter.java +++ b/src/main/java/millions/controller/fileIO/StockFileWriter.java @@ -2,6 +2,9 @@ import java.nio.file.Path; +/** + * Interface for writing stock data to a file. + */ public interface StockFileWriter { public void formatString(); public boolean write(Path path); diff --git a/src/main/java/millions/model/Exchange.java b/src/main/java/millions/model/Exchange.java index 57d91fd..869cff6 100644 --- a/src/main/java/millions/model/Exchange.java +++ b/src/main/java/millions/model/Exchange.java @@ -14,6 +14,9 @@ import millions.model.factories.SaleFactory; import millions.model.factories.TransactionFactory; +/** + * The stock exchange where players buy and sell shares. Manages stocks and simulates weekly price changes. + */ public class Exchange { private String name; private Map stocks; diff --git a/src/main/java/millions/model/ExchangeListener.java b/src/main/java/millions/model/ExchangeListener.java index b75057e..6731d45 100644 --- a/src/main/java/millions/model/ExchangeListener.java +++ b/src/main/java/millions/model/ExchangeListener.java @@ -1,5 +1,8 @@ package millions.model; +/** + * Listener for exchange events such as week advances and completed transactions. + */ public interface ExchangeListener { void onWeekAdvanced(int newWeek); diff --git a/src/main/java/millions/model/Player.java b/src/main/java/millions/model/Player.java index 09f43d4..2dfd684 100644 --- a/src/main/java/millions/model/Player.java +++ b/src/main/java/millions/model/Player.java @@ -5,6 +5,7 @@ import java.util.ArrayList; import java.util.List; +/** Player class. */ public class Player { private String name; private BigDecimal startingMoney; @@ -15,6 +16,11 @@ public class Player { public int weeksTraded; private final List listeners = new ArrayList<>(); + /** + * @param name Name of player + * @param startingMoney Amount of money the player starts with + * @throws IllegalArgumentException + */ public Player(String name, BigDecimal startingMoney) { this.name = name; this.startingMoney = startingMoney; @@ -31,6 +37,10 @@ public Player(String name, BigDecimal startingMoney) { } } + /** + * @param amount How much money to add + * @throws IllegalArgumentException + */ public void addMoney(BigDecimal amount) { if (amount == null || amount.compareTo(BigDecimal.ZERO) < 0) { throw new IllegalArgumentException("Amount cannot be null or negative"); @@ -39,6 +49,10 @@ public void addMoney(BigDecimal amount) { notifyMoneyChanged(); } + /** + * @param amount How much money to withdeaw + * @throws IllegalArgumentException + */ public void withdrawMoney(BigDecimal amount) { if (amount == null || amount.compareTo(BigDecimal.ZERO) < 0) { throw new IllegalArgumentException("Amount cannot be null or negative"); @@ -47,6 +61,9 @@ public void withdrawMoney(BigDecimal amount) { notifyMoneyChanged(); } + /** + * @return + */ public String getStatus() { // TODO dobbel sjekk logikken int weeksTraded = transactionArchive.countDistinctWeeks(); @@ -63,42 +80,69 @@ public String getStatus() { return status; } + /** + * @return + */ public String getName() { return this.name; } + /** + * @return + */ public BigDecimal getMoney() { return this.money; } + /** + * @return + */ public Portfolio getPortfolio() { return this.portfolio; } + /** + * @param share Share to be added + */ public void addShareToPortfolio(Share share) { this.portfolio.addShare(share); notifyPortfolioChanged(); notifyStatusChanged(); } + /** + * @param share Share to be removed + */ public void removeShareFromPortfolio(Share share) { this.portfolio.removeShare(share); notifyPortfolioChanged(); notifyStatusChanged(); } + /** + * @return + */ public BigDecimal getNetWorth() { return this.money.add(this.portfolio.getNetWorth()); } + /** + * @return + */ public TransactionArchive getTransactionArchive() { return this.transactionArchive; } + /** + * @param listener + */ public void addListener(PlayerListener listener) { listeners.add(listener); } + /** + * @param listener + */ public void removeListener(PlayerListener listener) { listeners.remove(listener); } diff --git a/src/main/java/millions/model/PlayerListener.java b/src/main/java/millions/model/PlayerListener.java index f576ac7..e397ead 100644 --- a/src/main/java/millions/model/PlayerListener.java +++ b/src/main/java/millions/model/PlayerListener.java @@ -2,6 +2,7 @@ import java.math.BigDecimal; +/** Listener for player state changes. */ public interface PlayerListener { void onMoneyChanged(BigDecimal newBalance); diff --git a/src/main/java/millions/model/Portfolio.java b/src/main/java/millions/model/Portfolio.java index 0c4af05..210f784 100644 --- a/src/main/java/millions/model/Portfolio.java +++ b/src/main/java/millions/model/Portfolio.java @@ -5,6 +5,7 @@ import java.util.List; import millions.model.calculators.SaleCalculator; +/** A collection of shares owned by a player. */ public class Portfolio { List shares; @@ -12,24 +13,42 @@ public Portfolio() { shares = new ArrayList<>(); } + /** + * @param share Share to be added + * @return + */ public boolean addShare(Share share) { return this.shares.add(share); } + /** + * @param share Share to be removed + * @return + */ public boolean removeShare(Share share) { return this.shares.remove(share); } + /** + * @return + */ public List getShares() { return this.shares; } + /** + * @param symbol + * @return + */ public List getShares(String symbol) { return this.shares.stream() .filter(share -> share.getStock().getSymbol().equals(symbol)) .toList(); } + /** + * @return + */ public BigDecimal getNetWorth() { BigDecimal total = BigDecimal.ZERO; for (Share share : shares) { @@ -39,6 +58,10 @@ public BigDecimal getNetWorth() { return total; } + /** + * @param share + * @return + */ public boolean contains(Share share) { return this.shares.contains(share); } diff --git a/src/main/java/millions/model/Purchase.java b/src/main/java/millions/model/Purchase.java index 59da2c0..8bbacb8 100644 --- a/src/main/java/millions/model/Purchase.java +++ b/src/main/java/millions/model/Purchase.java @@ -2,6 +2,9 @@ import millions.model.calculators.PurchaseCalculator; +/** + * A transaction representing the purchase of shares. + */ public class Purchase extends Transaction { public Purchase(Share share, int week) { diff --git a/src/main/java/millions/model/Sale.java b/src/main/java/millions/model/Sale.java index dbd3637..1c17745 100644 --- a/src/main/java/millions/model/Sale.java +++ b/src/main/java/millions/model/Sale.java @@ -2,6 +2,9 @@ import millions.model.calculators.SaleCalculator; +/** + * A transaction representing the sale of shares. + */ public class Sale extends Transaction { public Sale(Share share, int week) { diff --git a/src/main/java/millions/model/Share.java b/src/main/java/millions/model/Share.java index 615dc68..7967b95 100644 --- a/src/main/java/millions/model/Share.java +++ b/src/main/java/millions/model/Share.java @@ -2,11 +2,18 @@ import java.math.BigDecimal; +/** Represents a holding of a specific stock with a quantity and purchase price. */ public class Share { Stock stock; BigDecimal quantity; BigDecimal purchasePrice; + /** + * @param stock Which stock the share is for. + * @param quantity How many stocks + * @param purchasePrice Purchase price of the share + * @throws IllegalArgumentException + */ public Share(Stock stock, BigDecimal quantity, BigDecimal purchasePrice) { this.stock = stock; this.quantity = quantity; @@ -23,18 +30,28 @@ public Share(Stock stock, BigDecimal quantity, BigDecimal purchasePrice) { } } + /** Share() with int quantity */ public Share(Stock stock, int quantity, BigDecimal purchasePrice) { this(stock, BigDecimal.valueOf(quantity), purchasePrice); } + /** + * @return + */ public Stock getStock() { return this.stock; } + /** + * @return + */ public BigDecimal getQuantity() { return this.quantity; } + /** + * @return + */ public BigDecimal getPurchasePrice() { return this.purchasePrice; } diff --git a/src/main/java/millions/model/Stock.java b/src/main/java/millions/model/Stock.java index 65b70da..cc1a23f 100644 --- a/src/main/java/millions/model/Stock.java +++ b/src/main/java/millions/model/Stock.java @@ -4,11 +4,18 @@ import java.util.ArrayList; import java.util.List; +/** Stock */ public class Stock { String symbol; String company; List prices; + /** + * @param symbol Stock ticker symbol + * @param company company name + * @param prices List of prices + * @throws IllegalArgumentException + */ public Stock(String symbol, String company, List prices) { this.symbol = symbol; this.company = company; @@ -23,30 +30,49 @@ public Stock(String symbol, String company, List prices) { } } + /** Stock() with single price instead of list */ public Stock(String symbol, String company, BigDecimal initialPrice) { this(symbol, company, new ArrayList<>(List.of(initialPrice))); } + /** + * @return + */ public String getSymbol() { return this.symbol; } + /** + * @return + */ public String getCompany() { return this.company; } + /** + * @return + */ public BigDecimal getSalesPrice() { return this.prices.getLast(); } + /** + * @param price Sales price + */ public void addNewSalesPrice(BigDecimal price) { this.prices.add(price); } + /** + * @return + */ public List getHistoricalPrices() { return this.prices; } + /** + * @return + */ public BigDecimal getHighestPrice() { BigDecimal highestPrice = this.prices.get(0); for (BigDecimal price : this.prices) { @@ -57,6 +83,9 @@ public BigDecimal getHighestPrice() { return highestPrice; } + /** + * @return + */ public BigDecimal getLowestPrice() { BigDecimal lowestPrice = this.prices.get(0); for (BigDecimal price : this.prices) { @@ -67,6 +96,9 @@ public BigDecimal getLowestPrice() { return lowestPrice; } + /** + * @return + */ public BigDecimal getLatestPriceChange() { if (this.prices.size() < 2) { return BigDecimal.ZERO; diff --git a/src/main/java/millions/model/Transaction.java b/src/main/java/millions/model/Transaction.java index 8dfdd4f..6e1e8b1 100644 --- a/src/main/java/millions/model/Transaction.java +++ b/src/main/java/millions/model/Transaction.java @@ -2,6 +2,7 @@ import millions.model.calculators.TransactionCalculator; +/** Abstract base class for stock transactions */ public abstract class Transaction { private Share share; diff --git a/src/main/java/millions/model/TransactionArchive.java b/src/main/java/millions/model/TransactionArchive.java index 5e8b407..5ae7f9f 100644 --- a/src/main/java/millions/model/TransactionArchive.java +++ b/src/main/java/millions/model/TransactionArchive.java @@ -4,6 +4,7 @@ import java.util.List; import java.util.stream.Collectors; +/** Stores and queries commited transactions. */ public class TransactionArchive { List transactions; diff --git a/src/main/java/millions/model/calculators/PurchaseCalculator.java b/src/main/java/millions/model/calculators/PurchaseCalculator.java index 1f7b341..ab2608d 100644 --- a/src/main/java/millions/model/calculators/PurchaseCalculator.java +++ b/src/main/java/millions/model/calculators/PurchaseCalculator.java @@ -3,6 +3,7 @@ import java.math.BigDecimal; import millions.model.Share; +/** Calculates costs for a purchase transaction. Commission */ public class PurchaseCalculator implements TransactionCalculator { BigDecimal purchasePrice; BigDecimal quantity; diff --git a/src/main/java/millions/model/calculators/SaleCalculator.java b/src/main/java/millions/model/calculators/SaleCalculator.java index 9b0a84e..a907b46 100644 --- a/src/main/java/millions/model/calculators/SaleCalculator.java +++ b/src/main/java/millions/model/calculators/SaleCalculator.java @@ -4,6 +4,7 @@ import java.math.RoundingMode; import millions.model.Share; +/** Calculates costs for a sale transaction. Commission and profit tax */ public class SaleCalculator implements TransactionCalculator { BigDecimal purchasePrice; BigDecimal salesPrice; diff --git a/src/main/java/millions/model/calculators/TransactionCalculator.java b/src/main/java/millions/model/calculators/TransactionCalculator.java index d2d3917..9e6ecfb 100644 --- a/src/main/java/millions/model/calculators/TransactionCalculator.java +++ b/src/main/java/millions/model/calculators/TransactionCalculator.java @@ -2,6 +2,9 @@ import java.math.BigDecimal; +/** + * Interface for calculating transaction costs including gross, commission, tax, and total. + */ public interface TransactionCalculator { public BigDecimal calculateGross(); diff --git a/src/main/java/millions/model/factories/PurchaseFactory.java b/src/main/java/millions/model/factories/PurchaseFactory.java index e2b52f7..0af39aa 100644 --- a/src/main/java/millions/model/factories/PurchaseFactory.java +++ b/src/main/java/millions/model/factories/PurchaseFactory.java @@ -3,6 +3,9 @@ import millions.model.Purchase; import millions.model.Share; +/** + * Factory for creating purchase transactions. + */ public class PurchaseFactory extends TransactionFactory { public Purchase createTransaction(Share share, int week) { return new Purchase(share, week); diff --git a/src/main/java/millions/model/factories/SaleFactory.java b/src/main/java/millions/model/factories/SaleFactory.java index 4da40ee..6753a5e 100644 --- a/src/main/java/millions/model/factories/SaleFactory.java +++ b/src/main/java/millions/model/factories/SaleFactory.java @@ -3,6 +3,9 @@ import millions.model.Sale; import millions.model.Share; +/** + * Factory for creating sale transactions. + */ public class SaleFactory extends TransactionFactory { public Sale createTransaction(Share share, int week) { return new Sale(share, week); diff --git a/src/main/java/millions/model/factories/TransactionFactory.java b/src/main/java/millions/model/factories/TransactionFactory.java index 8c2eb5c..e77d31d 100644 --- a/src/main/java/millions/model/factories/TransactionFactory.java +++ b/src/main/java/millions/model/factories/TransactionFactory.java @@ -3,6 +3,9 @@ import millions.model.Share; import millions.model.Transaction; +/** + * Abstract factory for creating transactions. + */ public abstract class TransactionFactory { public abstract Transaction createTransaction(Share share, int week); } diff --git a/src/main/java/millions/view/StartView.java b/src/main/java/millions/view/StartView.java index 564883b..0c85a46 100644 --- a/src/main/java/millions/view/StartView.java +++ b/src/main/java/millions/view/StartView.java @@ -10,6 +10,7 @@ import javafx.stage.FileChooser; import javafx.stage.Stage; +/** The initial game setup screen where the player enters their info. */ public class StartView extends VBox { private TextField nameField; From cfbc5bc1640f32a234eab4af705e97f7400a3470 Mon Sep 17 00:00:00 2001 From: martin Date: Mon, 11 May 2026 14:04:26 +0200 Subject: [PATCH 16/16] fet: Adding javafx tab view with graph for stocks --- src/main/java/millions/App.java | 29 ++- .../millions/controller/GameController.java | 46 +++- src/main/java/millions/model/Exchange.java | 8 + .../millions/model/TransactionArchive.java | 4 + src/main/java/millions/view/GameView.java | 203 ++++++++++++++++++ src/main/java/millions/view/StartView.java | 37 +++- 6 files changed, 317 insertions(+), 10 deletions(-) create mode 100644 src/main/java/millions/view/GameView.java diff --git a/src/main/java/millions/App.java b/src/main/java/millions/App.java index 8ffc777..107ec61 100644 --- a/src/main/java/millions/App.java +++ b/src/main/java/millions/App.java @@ -1,14 +1,14 @@ package millions; +import java.math.BigDecimal; import javafx.application.Application; import javafx.scene.Scene; import javafx.stage.Stage; import millions.controller.GameController; +import millions.view.GameView; import millions.view.StartView; -/** - * Main JavaFX application entry point for the Millions stock trading game. - */ +/** Main JavaFX application entry point for the Millions stock trading game. */ public class App extends Application { @Override @@ -16,6 +16,29 @@ public void start(Stage stage) { GameController controller = new GameController(); StartView startView = new StartView(stage); + startView + .getStartButton() + .setOnAction( + event -> { + try { + controller.startGame( + startView.getName(), + new BigDecimal(startView.getStartingAmount()), + startView.getSelectedFile().toPath(), + startView.getPreRunWeeks()); + + GameView gameView = new GameView(controller); + controller.getPlayer().addListener(gameView); + controller.getExchange().addListener(gameView); + + Scene gameScene = new Scene(gameView, 1920, 1080); + stage.setScene(gameScene); + } catch (RuntimeException ex) { + System.err.println(ex); + System.exit(0); + } + }); + Scene scene = new Scene(startView, 400, 350); stage.setTitle("Millions"); stage.setScene(scene); diff --git a/src/main/java/millions/controller/GameController.java b/src/main/java/millions/controller/GameController.java index b6e5561..5638338 100644 --- a/src/main/java/millions/controller/GameController.java +++ b/src/main/java/millions/controller/GameController.java @@ -2,7 +2,9 @@ import java.math.BigDecimal; import java.nio.file.Path; +import java.util.Comparator; import java.util.List; +import java.util.stream.Collectors; import millions.controller.fileIO.CSVStockFileParser; import millions.controller.fileIO.StockFileReader; import millions.model.Exchange; @@ -14,14 +16,23 @@ public class GameController { private Player player; private Exchange exchange; - public void startGame(String name, BigDecimal startingMoney, Path stockFilePath) { + public void startGame( + String name, BigDecimal startingMoney, Path stockFilePath, int preRunWeeks) { + if (preRunWeeks < 0) { + throw new IllegalArgumentException("Pre run weeks cannot be negative"); + } + StockFileReader reader = new StockFileReader(stockFilePath); List lines = reader.readFile(); CSVStockFileParser parser = new CSVStockFileParser(lines); List stocks = parser.parse(); - player = new Player(name, startingMoney); exchange = new Exchange("Exchange", stocks); + for (int i = 0; i < preRunWeeks; i++) { + exchange.advance(); + } + + player = new Player(name, startingMoney); } public Player getPlayer() { @@ -31,4 +42,35 @@ public Player getPlayer() { public Exchange getExchange() { return exchange; } + + public List getStocks() { + return exchange.getStocks().values().stream() + .sorted(Comparator.comparing(Stock::getSymbol)) + .collect(Collectors.toList()); + } + + /** + * Gives alphabetic sort of findStocks + * + * @param searchTerm + * @return + */ + public List searchStocks(String searchTerm) { + if (searchTerm == null || searchTerm.isBlank()) { + return getStocks(); + } + return exchange.findStocks(searchTerm).stream() + .sorted(Comparator.comparing(Stock::getSymbol)) + .collect(Collectors.toList()); + } + + /** + * Get stocks with symbol + * + * @param symbol + * @return + */ + public Stock getStock(String symbol) { + return exchange.getStock(symbol); + } } diff --git a/src/main/java/millions/model/Exchange.java b/src/main/java/millions/model/Exchange.java index 869cff6..ef59694 100644 --- a/src/main/java/millions/model/Exchange.java +++ b/src/main/java/millions/model/Exchange.java @@ -69,6 +69,14 @@ public Transaction sell(Share share, Player player) { return sale; } + public String getName() { + return this.name; + } + + public int getWeekNumber() { + return this.weekNumber; + } + public Map getStocks() { return this.stocks; } diff --git a/src/main/java/millions/model/TransactionArchive.java b/src/main/java/millions/model/TransactionArchive.java index 5ae7f9f..6910a6c 100644 --- a/src/main/java/millions/model/TransactionArchive.java +++ b/src/main/java/millions/model/TransactionArchive.java @@ -25,6 +25,10 @@ public boolean isEmpty() { return transactions.isEmpty(); } + public List getTransactions() { + return new ArrayList<>(transactions); + } + public List getTransactions(int week) { return transactions.stream().filter(x -> x.getWeek() == week).collect(Collectors.toList()); } diff --git a/src/main/java/millions/view/GameView.java b/src/main/java/millions/view/GameView.java new file mode 100644 index 0000000..526c4f3 --- /dev/null +++ b/src/main/java/millions/view/GameView.java @@ -0,0 +1,203 @@ +package millions.view; + +import java.math.BigDecimal; +import java.util.List; +import javafx.scene.chart.LineChart; +import javafx.scene.chart.NumberAxis; +import javafx.scene.chart.XYChart; +import javafx.scene.control.Label; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.control.Tab; +import javafx.scene.control.TabPane; +import javafx.scene.control.TextField; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.VBox; +import millions.controller.GameController; +import millions.model.Exchange; +import millions.model.ExchangeListener; +import millions.model.Player; +import millions.model.PlayerListener; +import millions.model.Stock; +import millions.model.Transaction; + +/** Main game screen with tabs */ +public class GameView extends BorderPane implements PlayerListener, ExchangeListener { + + private final GameController controller; + private final Label playerNameLabel = new Label(); + private final Label weekLabel = new Label(); + private final Label moneyLabel = new Label(); + private final Label netWorthLabel = new Label(); + private final Label statusLabel = new Label(); + + private final TextField searchField = new TextField(); + private final ListView stocksList = new ListView<>(); + private final Label selectedStockLabel = new Label("Select a stock to see chart"); + private final NumberAxis xAxis = new NumberAxis(); + private final NumberAxis yAxis = new NumberAxis(); + private final LineChart stockChart = new LineChart<>(xAxis, yAxis); + + public GameView(GameController controller) { + this.controller = controller; + setTop(createHeader()); + setCenter(createTabs()); + configureStocksList(); + refreshAll(); + } + + private HBox createHeader() { + Label title = new Label("Millions"); + title.setStyle("-fx-font-size: 32px; -fx-font-weight: bold;"); + + HBox header = + new HBox(20, title, playerNameLabel, weekLabel, moneyLabel, netWorthLabel, statusLabel); + return header; + } + + private TabPane createTabs() { + TabPane tabPane = new TabPane(); + tabPane.getTabs().add(createStocksTab()); + tabPane.getTabs().add(createPortfolioTab()); + tabPane.getTabs().add(createTransactionsTab()); + return tabPane; + } + + private Tab createStocksTab() { + VBox leftPane = new VBox(10, new Label("Search"), searchField, stocksList); + + searchField.setPromptText("Search"); + searchField.textProperty().addListener((obs, oldVal, newVal) -> refreshStocks()); + + xAxis.setLabel("Week"); + xAxis.setAutoRanging(false); + xAxis.setLowerBound(1); // Stop week 0 + xAxis.setTickUnit(1); + yAxis.setLabel("Price"); + stockChart.setTitle("Price history"); + stockChart.setLegendVisible(false); + stockChart.setCreateSymbols(true); + stockChart.setAnimated(false); + stockChart.setPrefHeight(500); + + VBox rightPane = new VBox(10, selectedStockLabel, stockChart); + + HBox content = new HBox(12, leftPane, rightPane); + return new Tab("Stocks", content); + } + + private Tab createPortfolioTab() { + VBox content = new VBox(); + return new Tab("Portfolio", content); + } + + private Tab createTransactionsTab() { + VBox content = new VBox(); + return new Tab("Transactions", content); + } + + private void configureStocksList() { + stocksList.setCellFactory( + listView -> + new ListCell<>() { + @Override + protected void updateItem(Stock stock, boolean empty) { + super.updateItem(stock, empty); + if (empty || stock == null) { + setText(null); + } else { + setText(formatStock(stock)); + } + } + }); + + stocksList + .getSelectionModel() + .selectedItemProperty() + .addListener((obs, oldStock, newStock) -> showStockChart(newStock)); + } + + private void refreshAll() { + refreshPlayerInfo(); + refreshStocks(); + } + + private void refreshPlayerInfo() { + Player player = controller.getPlayer(); + Exchange exchange = controller.getExchange(); + + if (player == null || exchange == null) { + return; + } + + playerNameLabel.setText("Player: " + player.getName()); + weekLabel.setText("Week: " + exchange.getWeekNumber()); + moneyLabel.setText("Money: " + player.getMoney()); + netWorthLabel.setText("Net worth: " + player.getNetWorth()); + statusLabel.setText("Status: " + player.getStatus()); + } + + private void refreshStocks() { + List items = controller.searchStocks(searchField.getText()); + stocksList.getItems().setAll(items); + } + + private void showStockChart(Stock stock) { + stockChart.getData().clear(); + + if (stock == null) { + selectedStockLabel.setText("Select a stock to see chart"); + return; + } + + selectedStockLabel.setText( + stock.getSymbol() + + " - " + + stock.getCompany() + + " | Current: " + + stock.getSalesPrice() + + " | High: " + + stock.getHighestPrice() + + " | Low: " + + stock.getLowestPrice()); + + XYChart.Series series = new XYChart.Series<>(); + List prices = stock.getHistoricalPrices(); + xAxis.setUpperBound(Math.max(2, prices.size())); + for (int i = 0; i < prices.size(); i++) { + series.getData().add(new XYChart.Data<>(i + 1, prices.get(i))); + } + stockChart.getData().add(series); + } + + private String formatStock(Stock stock) { + return stock.getSymbol() + " - " + stock.getCompany() + " (" + stock.getSalesPrice() + ")"; + } + + // Listener callbacks update the shared header and the stocks tab. + @Override + public void onMoneyChanged(BigDecimal newBalance) { + refreshPlayerInfo(); + } + + @Override + public void onPortfolioChanged() { + refreshPlayerInfo(); + } + + @Override + public void onStatusChanged(String newStatus) { + refreshPlayerInfo(); + } + + @Override + public void onWeekAdvanced(int newWeek) { + refreshAll(); + } + + @Override + public void onTransactionCompleted(Transaction transaction) { + refreshPlayerInfo(); + } +} diff --git a/src/main/java/millions/view/StartView.java b/src/main/java/millions/view/StartView.java index 0c85a46..ffccb6f 100644 --- a/src/main/java/millions/view/StartView.java +++ b/src/main/java/millions/view/StartView.java @@ -1,6 +1,7 @@ package millions.view; import java.io.File; +import java.math.BigDecimal; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.control.Button; @@ -15,6 +16,7 @@ public class StartView extends VBox { private TextField nameField; private TextField startingAmountField; + private TextField preRunWeeksField; private File selectedFile; private Button filepickerButton; private Button startButton; @@ -24,17 +26,22 @@ public StartView(Stage stage) { setSpacing(12); setPadding(new Insets(40)); - nameField = new TextField(); + nameField = new TextField("user"); nameField.setPromptText("Player name:"); nameField.setMaxWidth(250); nameField.textProperty().addListener((obs, oldVal, newVal) -> checkStartButtonValid()); - - startingAmountField = new TextField(); + // Default to 50000 + startingAmountField = new TextField("50000"); startingAmountField.setPromptText("Starting amount:"); startingAmountField.setMaxWidth(250); startingAmountField .textProperty() .addListener((obs, oldVal, newVal) -> checkStartButtonValid()); + // Pre run weeks to run simulated weeks before the player starts + preRunWeeksField = new TextField("12"); + preRunWeeksField.setPromptText("Pre run weeks:"); + preRunWeeksField.setMaxWidth(250); + preRunWeeksField.textProperty().addListener((obs, oldVal, newVal) -> checkStartButtonValid()); filepickerButton = new Button(); filepickerButton.setText("Pick file"); @@ -49,6 +56,7 @@ public StartView(Stage stage) { if (file != null) { selectedFile = file; filepickerButton.setText(file.getName()); + checkStartButtonValid(); } }); @@ -58,9 +66,12 @@ public StartView(Stage stage) { Label title = new Label("Millions"); title.setStyle("-fx-font-size: 32px; -fx-font-weight: bold;"); - getChildren().addAll(title, nameField, startingAmountField, filepickerButton, startButton); + getChildren() + .addAll( + title, nameField, startingAmountField, preRunWeeksField, filepickerButton, startButton); } + /** Enables/Disables start button */ private void checkStartButtonValid() { boolean valid = true; @@ -68,8 +79,20 @@ private void checkStartButtonValid() { valid = false; } + if (selectedFile == null) { + valid = false; + } + try { - Integer.valueOf(startingAmountField.getText()); + new BigDecimal(startingAmountField.getText()); + } catch (NumberFormatException e) { + valid = false; + } + + try { + if (Integer.parseInt(preRunWeeksField.getText()) < 0) { + valid = false; + } } catch (NumberFormatException e) { valid = false; } @@ -85,6 +108,10 @@ public String getStartingAmount() { return startingAmountField.getText(); } + public int getPreRunWeeks() { + return Integer.parseInt(preRunWeeksField.getText()); + } + public File getSelectedFile() { return selectedFile; }