diff --git a/millions/millions.iml b/millions/millions.iml
index f23133a..abe781f 100644
--- a/millions/millions.iml
+++ b/millions/millions.iml
@@ -5,7 +5,4 @@
-
-
-
\ No newline at end of file
diff --git a/millions/pom.xml b/millions/pom.xml
index bd65466..117b7d9 100644
--- a/millions/pom.xml
+++ b/millions/pom.xml
@@ -26,7 +26,7 @@
org.openjfxjavafx-controls
- 25.0.2
+ 25.0.1
@@ -46,6 +46,9 @@
org.openjfxjavafx-maven-plugin0.0.8
+
+ no.ntnu.gruppe53.App
+ org.apache.maven.plugins
diff --git a/millions/src/main/java/no/ntnu/gruppe53/App.java b/millions/src/main/java/no/ntnu/gruppe53/App.java
index 4205122..3170ab6 100644
--- a/millions/src/main/java/no/ntnu/gruppe53/App.java
+++ b/millions/src/main/java/no/ntnu/gruppe53/App.java
@@ -1,10 +1,41 @@
package no.ntnu.gruppe53;
+import javafx.application.Application;
+import javafx.stage.Stage;
+import no.ntnu.gruppe53.controller.StartViewController;
+import no.ntnu.gruppe53.controller.ViewController;
+import no.ntnu.gruppe53.view.StartView;
+
/**
- * Hello world!
+ * Represents the main app of the application.
+ *
+ *
Sets up necessary components and shows the first view
+ * of the application: {@link StartView}.
*/
-public class App {
- public static void main(String[] args) {
- System.out.println("Hello World!");
- }
+public class App extends Application {
+ @Override
+ public void start(Stage stage) {
+ ViewController vm = new ViewController(stage);
+
+ StartView startView = new StartView(vm);
+
+ new StartViewController(startView, vm);
+
+
+ vm.addView("start", startView);
+
+ vm.switchView("start");
+
+ stage.setTitle("Millions - the Stock Game");
+ stage.show();
+ }
+
+ /**
+ * Launches the program.
+ *
+ * @param args arguments
+ */
+ static void main(String[] args) {
+ launch();
+ }
}
diff --git a/millions/src/main/java/no/ntnu/gruppe53/Exchange.java b/millions/src/main/java/no/ntnu/gruppe53/Exchange.java
deleted file mode 100644
index 0f729a5..0000000
--- a/millions/src/main/java/no/ntnu/gruppe53/Exchange.java
+++ /dev/null
@@ -1,190 +0,0 @@
-package no.ntnu.gruppe53;
-
-import java.math.BigDecimal;
-import java.math.RoundingMode;
-import java.util.*;
-import java.util.stream.Collectors;
-
-public class Exchange {
- private final String name;
- private int week;
- private final Map stockMap;
- private final Random random;
-
- public Exchange(String name, List stocks) {
- if (name == null || name.isBlank()) {
- throw new IllegalArgumentException("Exchange name cannot be null or blank");
- }
-
- if (stocks == null) {
- throw new IllegalArgumentException("Stock list cannot be null");
- }
-
- this.name = name;
- this.week = 1;
- this.stockMap = new HashMap<>();
- this.random = new Random();
-
- for (Stock stock : stocks) {
- if (stock == null) {
- throw new IllegalArgumentException("Stock cannot be null");
- }
-
- if (stockMap.containsKey(stock.getSymbol())) {
- throw new IllegalArgumentException("Duplicate stock symbol: " + stock.getSymbol());
- }
-
- stockMap.put(stock.getSymbol(), stock);
- }
-
- }
-
- public String getName() {
- return this.name;
- }
-
- public int getWeek() {
- return this.week;
- }
-
-
- // Returns Boolean value of if the exchange has the stock.
- public boolean hasStock(String symbol) {
-
- return stockMap.containsKey(symbol);
- }
-
- // Returns stock that corresponds to given symbol
- public Stock getStock(String symbol) {
- if (symbol == null || !stockMap.containsKey(symbol)) {
- throw new IllegalArgumentException("Stock not found: " + symbol);
- }
- return stockMap.get(symbol);
- }
-
- public List findStocks(String searchTerm) {
- //Wait for merging of other branches before implementation
- if (searchTerm == null) {
- return Collections.emptyList();
- }
-
- String search = searchTerm.toLowerCase();
-
- return stockMap.values().stream().filter(stock ->
- stock.getSymbol().toLowerCase().contains(search) ||
- stock.getCompany().toLowerCase().contains(search)).collect(Collectors.toList());
-
- }
-
- public Transaction buy(String symbol, BigDecimal quantity, Player player) {
- //Wait for merging of other branches before implementation
-
- if (player == null) {
- throw new IllegalArgumentException("Player cannot be null");
- }
-
- if (quantity == null || quantity.compareTo(BigDecimal.ZERO) <= 0) {
- throw new IllegalArgumentException("Quantity must be positive");
- }
-
- Stock stock = getStock(symbol);
-
- Share share = new Share(stock, quantity, stock.getSalesPrice());
-
- Transaction transaction = new Purchase(share, week);
-
- transaction.commit(player);
-
- return transaction;
-
- }
-
- public Transaction sell(Share share, Player player) {
- //Wait for merging of other branches before implementation
-
-
-
- if (player == null) {
- throw new IllegalArgumentException("Player cannot be null");
- }
-
- if (share == null) {
- throw new IllegalArgumentException("Share cannot be null");
- }
-
- Transaction transaction = new Sale(share, week);
-
- transaction.commit(player);
-
- return transaction;
- }
-
- public void advance() {
- this.week++;
-
- double negativePriceChange = -0.05;
- double positivePriceChange = 0.1;
-
- for (Stock stock : stockMap.values()) {
-
- BigDecimal currentPrice = stock.getSalesPrice();
-
- // Bruh java gjør random shit rart as
- double randomPriceMultiplier = negativePriceChange +
- (positivePriceChange * random.nextDouble());
-
- BigDecimal bdRandomPriceMultiplier = BigDecimal.valueOf(1 + randomPriceMultiplier);
-
- BigDecimal changedPrice = currentPrice.multiply(bdRandomPriceMultiplier)
- .setScale(2, RoundingMode.HALF_UP);
-
- if (changedPrice.compareTo(BigDecimal.ZERO) <= 0) {
- changedPrice = new BigDecimal(1);
- }
-
- stock.addNewSalesPrice(changedPrice);
- }
- }
-
- /**
- * Lists the given amount of stocks sorted by highest increase in sales price since last week.
- * @param entries amount of stocks to be shown in list
- * @return a descending list of stocks with the highest price increase since last week
- * @throws IllegalArgumentException if number of entries in list is negative
- */
- public List getGainers(int entries) {
- if (entries < 0) {
- throw new IllegalArgumentException("Amount of entries in list cannot be negative.");
- }
-
- return stockMap.values().stream()
- .sorted(Comparator.comparing(Stock::getLatestPriceChange).reversed())
- .filter(stock -> stock.getLatestPriceChange().compareTo(BigDecimal.ZERO) > 0)
- .limit(entries)
-
- .collect(Collectors.toList());
- }
-
- /**
- * Lists the given amount of stocks sorted by the stocks with the lowest price increase (or
- * highest price decrease)
- * @param entries amount of stocks to be shown in the list
- * @return a descending list of stocks with the highest price decrease or
- * lowest price increase
- * @throws IllegalArgumentException if number of entries in list is negative
- */
- public List getLosers(int entries) {
- if (entries < 0) {
- throw new IllegalArgumentException("Amount of entries in list cannot be negative.");
- }
-
- return stockMap.values().stream()
- .sorted(Comparator.comparing(Stock::getLatestPriceChange))
- .filter(stock -> stock.getLatestPriceChange().compareTo(BigDecimal.ZERO) < 0)
- .limit(entries)
-
- .collect(Collectors.toList());
- }
-
-
-}
diff --git a/millions/src/main/java/no/ntnu/gruppe53/FileHandler.java b/millions/src/main/java/no/ntnu/gruppe53/FileHandler.java
deleted file mode 100644
index 8133e72..0000000
--- a/millions/src/main/java/no/ntnu/gruppe53/FileHandler.java
+++ /dev/null
@@ -1,128 +0,0 @@
-package no.ntnu.gruppe53;
-
-import java.io.BufferedReader;
-import java.io.BufferedWriter;
-import java.io.IOException;
-import java.math.BigDecimal;
-import java.nio.charset.Charset;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Handles file operations for the application.
- */
-
-public class FileHandler {
-
- /**
- * Writes a list of {@link Stock}s to a text-file. Class is structed for {@code .csv} files.
- *
- * @param stockList the list of stocks to be converted to text
- * @param path output path for the file
- * @param filename name of the file (not including extension)
- */
- public static void writeStocksToFile(List stockList, String path, String filename) {
- String pathString = path + filename;
- Path filePath = Paths.get(pathString);
-
- try (BufferedWriter writer = Files.newBufferedWriter(filePath, Charset.defaultCharset())) {
- if (!Files.exists(filePath)) {
- Files.createDirectories(filePath);
- }
-
- writer.write("#Ticker,Name,Price");
- writer.newLine();
- writer.newLine();
-
- for (Stock stock : stockList) {
- String line = String.join(",", stock.getSymbol(), stock.getCompany(),
- stock.getSalesPrice().toString());
- writer.write(line);
- writer.newLine();
- }
-
- } catch (IOException e) {
- System.out.println("ERROR: Something went wrong while writing the csv file.");
- System.out.println("Message: " + e.getMessage());
- }
- }
-
- /**
- * Reads a .csv file and converts it to a list of {@code Stock} objects.
- *
- * @param path path to the file to be read
- * @param filename name of the file to be read (including extension)
- * @return a list of {@code Stock} objects converted from text
- */
- public static List readStocksFromFile(String path, String filename) {
- List stocks = new ArrayList<>();
- String pathString = path + filename;
- Path filePath = Paths.get(pathString);
-
- try (BufferedReader reader = Files.newBufferedReader(filePath, Charset.defaultCharset())) {
- String line;
- String symbol;
- String name;
- BigDecimal price;
-
- while ((line = reader.readLine()) != null) {
- symbol = null;
- name = null;
- price = null;
- int counter = 0;
- String trimValue = line.trim();
-
- if (trimValue.isBlank() || (line.charAt(0) == '#')) {
- continue;
- }
-
- String[] values = line.split(",");
-
- if (values.length != 3) {
- System.out.println("ERROR: Bad line in csv: ");
- System.out.print(line);
- System.out.println("There should be 3 values total: " +
- "symbol, name, price.");
- continue;
- }
-
- for (String value : values) {
- trimValue = value.trim();
-
- switch (counter) {
- case 0:
- symbol = trimValue;
- break;
- case 1:
- name = trimValue;
- break;
- case 2:
- try {
- price = new BigDecimal(trimValue);
- }
- catch (NumberFormatException e) {
- System.out.println("ERROR: Not a number. Last column in .csv file " +
- "needs to be the price of the stock (number). Skipping...");
- price = null;
- break;
- }
- }
- counter++;
- }
- if (symbol == null || symbol.isBlank() || name == null || name.isBlank() ||
- price == null || price.compareTo(BigDecimal.ZERO) <= 0) {
- continue;
- }
- stocks.add(new Stock(symbol, name, price));
- }
- }
- catch (IOException e) {
- System.out.println("ERROR: Something went wrong while reading the csv file.");
- System.out.println("Message: " + e.getMessage());
- }
- return stocks;
- }
-}
diff --git a/millions/src/main/java/no/ntnu/gruppe53/Player.java b/millions/src/main/java/no/ntnu/gruppe53/Player.java
deleted file mode 100644
index f10e8a3..0000000
--- a/millions/src/main/java/no/ntnu/gruppe53/Player.java
+++ /dev/null
@@ -1,165 +0,0 @@
-package no.ntnu.gruppe53;
-
-import java.math.BigDecimal;
-import java.math.RoundingMode;
-
-/**
- * Represents a participant in the trading game.
- *
- * A {@code Player} has a name, a monetary balance, a {@link Portfolio}
- * of owned {@link Share}s, and a {@link TransactionArchive} containing
- * a history of the player's transactions.
- *
- *
- *
- * The player starts with an initial amount of money and may gain or
- * lose funds through transactions.
- *
- */
-
-public class Player {
- private final String name;
- private final BigDecimal startingMoney;
- private BigDecimal money;
- private final Portfolio portfolio;
- private final TransactionArchive transactionArchive;
-
- /**
- * Creates a player with a given starting balance.
- *
- * @param name the player's name; must not be {@code null} or blank
- * @param startingMoney the initial monetary balance of the
- * player; must not be {@code null} or negative
- * @throws IllegalArgumentException if {@code name} is {@code null} or
- * blank, or if {@code startingMoney}
- * is {@code null}, negative, or 0
- */
-
- public Player(String name, BigDecimal startingMoney) {
- if (name == null || name.isBlank()) {
- throw new IllegalArgumentException("Player name must not be " +
- "null or empty.");
- }
-
- if (startingMoney == null || startingMoney.signum() <= 0 ) {
- throw new IllegalArgumentException("Starting money must not be " +
- "null, negative, nor 0.");
- }
- this.name = name;
- this.startingMoney = startingMoney;
- this.money = startingMoney;
- this.portfolio = new Portfolio();
- this.transactionArchive = new TransactionArchive();
- }
-
- /**
- * Returns the full name of the player.
- *
- * @return the player's name
- */
-
- public String getName() {
- return this.name;
- }
-
- /**
- * Returns the player's current monetary balance.
- *
- * @return the player's current balance.
- */
-
- public BigDecimal getMoney() {
- return this.money;
- }
-
- /**
- * Increases the players monetary balance by the given amount.
- *
- * Money added must be a {@code positive} number.
- *
- *
- * @param amount the amount to add; must be positive and not {@code null}
- * @throws IllegalArgumentException if {@code amount] is {@code null}
- * or not positive
- */
-
- public void addMoney(BigDecimal amount) {
- if (amount == null) {
- throw new IllegalArgumentException("Amount must not be null.");
- }
-
- if (amount.signum() <= 0) {
- throw new IllegalArgumentException("Amount must be positive.");
- }
-
- this.money = this.money.add(amount);
- }
-
- /**
- * Decreases the player's monetary balance by the given amount.
- *
- * The player's monetary balance must not decrease below {@code 0}
- * (overdrawn not possible).
- *
- *
- * @param amount the amount to subtract; must not be negative or {@code null}
- * @throws IllegalArgumentException if the amount is not positive, {@code null}
- * , or insufficient funds are available
- */
-
- public void withdrawMoney(BigDecimal amount) {
- if (amount == null) {
- throw new IllegalArgumentException("Amount must not be null.");
- }
-
- if (amount.signum() <= 0) {
- throw new IllegalArgumentException("Amount must be positive.");
- }
-
- if (this.money.subtract(amount).signum() < 0) {
- throw new IllegalArgumentException("Insufficient funds.");
- }
-
- this.money = this.money.subtract(amount);
- }
-
- /**
- * Return's the player's portfolio of shares.
- *
- * @return the player's portfolio
- */
-
- public Portfolio getPortfolio() {
- return this.portfolio;
- }
-
- /**
- * Returns the archive of the player's transaction history.
- *
- * @return the player's transaction archive
- */
-
- public TransactionArchive getTransactionArchive() {
- return this.transactionArchive;
- }
-
- public BigDecimal getNetWorth() {
- return this.money.add(this.portfolio.getNetWorth());
- }
-
- public String getStatus(Exchange exchange) {
- String status = "Novice";
-
- if ((getNetWorth().divide(this.startingMoney, RoundingMode.HALF_EVEN).compareTo(new BigDecimal("1.20"))
- >= 0) && exchange.getWeek() >= 10) {
- status = "Investor";
- }
-
- if ((getNetWorth().divide(this.startingMoney, RoundingMode.HALF_EVEN).compareTo(new BigDecimal("2"))
- >= 0) && exchange.getWeek() >= 20) {
- status = "Speculator";
- }
-
- return status;
- }
-}
diff --git a/millions/src/main/java/no/ntnu/gruppe53/Portfolio.java b/millions/src/main/java/no/ntnu/gruppe53/Portfolio.java
deleted file mode 100644
index 2f40774..0000000
--- a/millions/src/main/java/no/ntnu/gruppe53/Portfolio.java
+++ /dev/null
@@ -1,145 +0,0 @@
-package no.ntnu.gruppe53;
-
-import java.math.BigDecimal;
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Represents the portfolio of the Player, containing information
- * about which {@link Share}s are owned.
- *
- * A {@code Portfolio} manages a mutable collection of shares, allowing
- * shares to be added, removed, and queried.
- *
- *
- *
- * Shares are associated with unique Transaction, however, multiple
- * shares from the same stock can exist in the portfolio.
- *
- */
-
-public class Portfolio {
- private final List shares;
-
- /**
- * Creates an empty portfolio for the player.
- */
-
- public Portfolio() {
- this.shares = new ArrayList<>();
- }
-
- /**
- * Adds a share to the player's portfolio.
- *
- * The {@link Stock} must not be {@code null} and the share quantity
- * and purchase price must be above zero.
- *
- *
- * @param share the share owned by the player; must not be {@code null}
- * @return {@code true} if the share was added successfully,
- * {@code false} otherwise
- * @throws IllegalArgumentException if {@code share} is {@code null}
- */
-
- public boolean addShare(Share share) {
- if (share == null) {
- throw new IllegalArgumentException("Share cannot be null");
- }
-
- this.shares.add(share);
-
- return this.getShares().getLast().equals(share);
- }
-
- /**
- * Removes a share from the player's portfolio.
- *
- * The list must already exist in the portfolio to be successfully
- * removed.
- *
- *
- * @param share the share being removed; must not be {@code null}
- * @return {@code true} if the share removal was successful,
- * {@code false} otherwise.
- * @throws IllegalArgumentException if {@code share} is {@code null}
- */
- public boolean removeShare(Share share) {
- if (share == null) {
- throw new IllegalArgumentException("Share cannot be null");
- }
-
- for (Share s : this.shares) {
- if (s.equals(share)) {
- this.shares.remove(s);
- return true;
- }
- }
- return false;
- }
-
- /**
- * Returns a list of the shares currently in the player's portfolio.
- *
- * @return a list of the shares in this portfolio
- */
- public List getShares() {
- return this.shares;
- }
-
- /**
- * Returns a list of the shares that belongs to the stock with the
- * given ticker symbol.
- *
- * @param symbol the ticker symbol of the stock to search for; must not be
- * {@code null} or blank
- * @return a list of the player's shares in the stock with the given
- * stock symbol; an empty list if none are found
- * @throws IllegalArgumentException if {@code symbol} is {@code null} or blank
- */
- public List getShares(String symbol) {
- if (symbol == null || symbol.isBlank()) {
- throw new IllegalArgumentException("Symbol cannot be null or blank");
- }
-
- var tempSymbols = new ArrayList();
- for (Share s : this.shares) {
- if (s.getStock().getSymbol().equals(symbol)) {
- tempSymbols.add(s);
- }
- }
- return tempSymbols;
- }
-
- /**
- * Determines whether the player's portfolio contains the specified
- * share.
- *
- * @param share the share to search for in this portfolio
- * @return {@code true} if the player's portfolio contains the share
- * {@code false} otherwise
- */
-
- public boolean contains(Share share) {
- for (Share s : this.shares) {
- if (s.equals(share)) {
- return true;
- }
- }
- return false;
- }
-
- /**
- * Calculates the net total value of the shares in the portfolio.
- *
Calculations are done using a {@link SaleCalculator}.
- *
- * @return the total sale value of the portfolio
- */
- public BigDecimal getNetWorth() {
-
- return this.shares.stream()
- .map(share -> new SaleCalculator(share).calculateTotal())
- .reduce(BigDecimal.ZERO, BigDecimal::add);
- }
-
-}
diff --git a/millions/src/main/java/no/ntnu/gruppe53/Purchase.java b/millions/src/main/java/no/ntnu/gruppe53/Purchase.java
deleted file mode 100644
index 0b622e2..0000000
--- a/millions/src/main/java/no/ntnu/gruppe53/Purchase.java
+++ /dev/null
@@ -1,62 +0,0 @@
-package no.ntnu.gruppe53;
-
-import java.math.BigDecimal;
-
-/**
- * Represents a purchase {@link Transaction} where a {@link Player} purchases
- * a {@link Share}
- *
- * A purchase can only be commited if:
- *
- *
The player has sufficient funds.
- *
The transaction has not already been committed.
- *
- *
- */
-
-public class Purchase extends Transaction{
-
- /**
- * Constructs a purchase transaction for the given share and week.
- *
- * @param share the share to be purchased; must not be {@code null}
- * @param week the week when the purchase transaction occurs; must be 1 or greater
- */
-
- public Purchase(Share share, int week) {
- super(share, week, new PurchaseCalculator(share));
- }
-
- /**
- * Commits the purchase to the given player, making the
- * transaction unique.
- *
- * If the transaction is not already committed, and the player
- * has sufficient funds, the transaction will:
- *
- *
Withdraw money from the player
- *
Add the share to the player's {@link Portfolio}
- *
Archive the transaction in the player's
- * {@link TransactionArchive}
- *
- *
- * @param player the player performing the transaction; must not be {@code null}
- * @throws IllegalArgumentException if {@code player} is {@code null}
- */
-
- @Override
- public void commit(Player player) {
- if (player == null) {
- throw new IllegalArgumentException("Player cannot be null.");
- }
-
- if (!this.isCommitted() && ((player.getMoney().subtract(this.getCalculator().calculateTotal()))
- .compareTo(BigDecimal.ZERO) >= 0)) {
-
- player.withdrawMoney(this.getCalculator().calculateTotal());
- player.getPortfolio().addShare(this.getShare());
- player.getTransactionArchive().add(this);
- super.commit(player);
- }
- }
-}
\ No newline at end of file
diff --git a/millions/src/main/java/no/ntnu/gruppe53/PurchaseCalculator.java b/millions/src/main/java/no/ntnu/gruppe53/PurchaseCalculator.java
deleted file mode 100644
index 8620cb5..0000000
--- a/millions/src/main/java/no/ntnu/gruppe53/PurchaseCalculator.java
+++ /dev/null
@@ -1,69 +0,0 @@
-package no.ntnu.gruppe53;
-
-import java.math.BigDecimal;
-
-/*
-PurchaseCalculator class:
-
-Implements from TransactionCalculator
-
-Which contains:
-1. Constructor which passes the values of a Share object
- to its local variables.
-2. calculateGross() function to calculate the gross value
-3. calculateCommission() function to calculate the commission value
-4. calculateTax() function which does nothing as there is no tax
-5. calculateTotal() function which calculates the total purchase price
- by adding all the calculations together.
-
- */
-public class PurchaseCalculator implements TransactionCalculator{
- private final BigDecimal purchasePrice;
- private final BigDecimal quantity;
-
-
- //Takes a Share object to pass its Purchase and Quantity Values to
- //Its local private variables
- public PurchaseCalculator(Share share) {
-
- this.purchasePrice = share.getPurchasePrice();
- this.quantity = share.getQuantity();
-
- }
-
- // Multiplies quantity with the purchase price to get the gross value
- @Override
- public BigDecimal calculateGross(){
- return this.quantity.multiply(this.purchasePrice);
- }
-
- //Takes the gross value calculated in calculateGross()
- //And multiplies it with the 0.05% commission on purchase to
- //get the total value of the commission
- @Override
- public BigDecimal calculateCommission() {
- //0.5% commission on purchase
- BigDecimal commission = new BigDecimal("0.005");
-
- return calculateGross().multiply(commission);
- }
-
- //No tax on purchase
- @Override
- public BigDecimal calculateTax() {
- return new BigDecimal(0);
- }
-
-
- //Calculates the total purchase price by adding
- // Gross + Commission + Tax
- @Override
- public BigDecimal calculateTotal() {
-
- return calculateGross().add(calculateCommission()).add(calculateTax());
- }
-
-
-
-
-}
diff --git a/millions/src/main/java/no/ntnu/gruppe53/Sale.java b/millions/src/main/java/no/ntnu/gruppe53/Sale.java
deleted file mode 100644
index 00787d0..0000000
--- a/millions/src/main/java/no/ntnu/gruppe53/Sale.java
+++ /dev/null
@@ -1,57 +0,0 @@
-package no.ntnu.gruppe53;
-
-/**
- * Represents a sale {@link Transaction} where a {@link Player} sells
- * a {@link Share}
- *
- * A sale can only be commited if:
- *
- *
The share is in the players {@link Portfolio}
- *
The transaction has not already committed
- *
- *
- */
-
-public class Sale extends Transaction{
-
- /**
- * Constructs a sale transaction for the given share and week.
- *
- * @param share the share to be sold; must not be {@code null}
- * @param week the week when the sale transaction occurs; must be greater than 0
- */
-
- public Sale(Share share, int week) {
- super(share, week, new SaleCalculator(share));
- }
-
- /**
- * Commits the sale to the given player, making the
- * transaction unique.
- *
- * If the player ows the share, the transaction will:
- *
- *
Add money to the player
- *
Remove the share from the player's portfolio
- *
Archive the transaction in the player's {@link TransactionArchive}
- *
- *
- * @param player the player performing the transaction; must not be {@code null}
- * @throws IllegalArgumentException if {@code player} is {@code null}
- */
-
- @Override
- public void commit(Player player) {
- if (player == null) {
- throw new IllegalArgumentException("Player cannot be null");
- }
-
- if (!this.isCommitted() && (player.getPortfolio().contains(this.getShare()))
- ) {
- player.addMoney(this.getCalculator().calculateTotal());
- player.getPortfolio().removeShare(this.getShare());
- player.getTransactionArchive().add(this);
- super.commit(player);
- }
- }
-}
diff --git a/millions/src/main/java/no/ntnu/gruppe53/SaleCalculator.java b/millions/src/main/java/no/ntnu/gruppe53/SaleCalculator.java
deleted file mode 100644
index b3b8345..0000000
--- a/millions/src/main/java/no/ntnu/gruppe53/SaleCalculator.java
+++ /dev/null
@@ -1,80 +0,0 @@
-package no.ntnu.gruppe53;
-
-import java.math.BigDecimal;
-
-/*
-SaleCalculator class:
-
-Implements from Interface TransactionCalculator
-
-Which contains:
-1. Constructor which passes the values of a Share object
- to its local variables.
-2. calculateGross() function to calculate the gross value
-3. calculateCommision() function to calculate the commission value
-4. calculateTax() function which calculates the 30% tax on winnings
-5. calculateTotal() function which calculates the total sales price
- by subtracting commission and tax from the gross value
-
- */
-
-
-
-public class SaleCalculator implements TransactionCalculator{
- private final BigDecimal purchasePrice;
- private final BigDecimal salesPrice;
- private final BigDecimal quantity;
-
- //Takes a Share object to pass its Quantity, Purchase- and SalesPrice Values to
- //Its local private variables
- public SaleCalculator(Share share) {
- this.purchasePrice = share.getPurchasePrice();
- this.quantity = share.getQuantity();
- this.salesPrice = share.getStock().getSalesPrice();
- }
-
-
- //Multiplies quntity with the sales price to get the gross value
- @Override
- public BigDecimal calculateGross() {
-
- return this.quantity.multiply(this.salesPrice);
- }
-
- //Takes the gross value calculated in calculateGross()
- //And multiplies it with the 0.1% commission on sale to
- //get the total value of the commission
- @Override
- public BigDecimal calculateCommission() {
-
- //0.1% commission
- BigDecimal commision = new BigDecimal("0.01");
-
- return calculateGross().multiply(commision);
- }
-
-
- //Calculates the tax on winnings, winnings are defined as
- //Gross - commission - (purchaseprice * quantity)
- //Tax is therefore calculated as winnings * tax(30%)
- @Override
- public BigDecimal calculateTax() {
-
-
- BigDecimal tax = new BigDecimal("0.30");
-
- // (Gross - commission - (Purchase price - quantity)) * 30% tax
- return calculateGross().subtract(calculateCommission())
- .subtract(this.purchasePrice.multiply(this.quantity)).multiply(tax);
-
- }
-
- //Calculates the total sales price by subtracting
- //Gross - Commission - Tax
- @Override
- public BigDecimal calculateTotal() {
-
- return calculateGross().subtract(calculateCommission()).subtract(calculateTax());
-
- }
-}
diff --git a/millions/src/main/java/no/ntnu/gruppe53/Share.java b/millions/src/main/java/no/ntnu/gruppe53/Share.java
deleted file mode 100644
index 6e3e21e..0000000
--- a/millions/src/main/java/no/ntnu/gruppe53/Share.java
+++ /dev/null
@@ -1,75 +0,0 @@
-package no.ntnu.gruppe53;
-
-import java.math.BigDecimal;
-
-/**
- * Represents a holding of a specific {@link Stock}.
- *
- * A {@code Share} contains the quantity of shares owned and
- * the price per share at the time of purchase. Future price
- * fluctuations are not included in share.
- *
- */
-
-public class Share {
- private final Stock stock;
- private final BigDecimal quantity;
- private final BigDecimal purchasePrice;
-
- /**
- * Creates a share for a given stock.
- *
- * @param stock the stock associated with the share; must not be {@code null}
- * @param quantity the number of shares purchased; must not be {@code null}, negative, or zero
- * @param purchasePrice price per share at the time of purchase; must not be {@code null}, negative or zero
- * @throws IllegalArgumentException if {@code stock} is {@code null}, or {@code quantity} is {@code null},
- * negative, or zero, or {@code purchasePrice} is {@code null}, negative,
- * or zero
- */
-
- public Share(Stock stock, BigDecimal quantity, BigDecimal purchasePrice) {
- if (stock == null) {
- throw new IllegalArgumentException("Stock cannot be null.");
- }
-
- if (quantity == null || quantity.signum() <= 0) {
- throw new IllegalArgumentException("Quantity cannot be null, negative, or zero.");
- }
-
- if (purchasePrice == null || purchasePrice.signum() <= 0) {
- throw new IllegalArgumentException("PurchasePrice cannot be null, negative, or zero.");
- }
-
- this.stock = stock;
- this.quantity = quantity;
- this.purchasePrice = purchasePrice;
- }
-
- /**
- * Returns the stock the share is associated with.
- *
- * @return the stock
- */
- public Stock getStock() {
- return stock;
- }
-
- /**
- * Returns the quantity of shares owned in the stock.
- *
- * @return the quantity of shares
- */
-
- public BigDecimal getQuantity() {
- return quantity;
- }
-
- /**
- * Returns the purchase price per share for the stock at time of purchase.
- *
- * @return the purchase price of the shares.
- */
- public BigDecimal getPurchasePrice() {
- return purchasePrice;
- }
-}
diff --git a/millions/src/main/java/no/ntnu/gruppe53/Stock.java b/millions/src/main/java/no/ntnu/gruppe53/Stock.java
deleted file mode 100644
index 71d93f4..0000000
--- a/millions/src/main/java/no/ntnu/gruppe53/Stock.java
+++ /dev/null
@@ -1,148 +0,0 @@
-package no.ntnu.gruppe53;
-
-import java.math.BigDecimal;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-/**
- * Represents a stock with its ticker symbol, company name,
- * and a list of sales prices.
- *
- * The list is a historical representation of price changes
- * over time for the given stock.
- *
- *
- * The most recently added price represents the current market price.
- *
- *
- *
- * A {@code Stock} instance always contains at least one price,
- * instantiated at construction.
- *
- */
-
-public class Stock {
- private final String symbol;
- private final String company;
- private final List prices;
-
- /**
- * Creates a stock with an already defined sales price.
- *
- * @param symbol the stock's unique ticker symbol (e.g. "AAPL"); must not be {@code null} or blank
- * @param company the full name of the company; must not be {@code null} or blank
- * @param salesPrice the current price per share; must not be {@code null}, negative, or zero
- * @throws IllegalArgumentException if {@code symbol} is {@code null} or blank, {@code company} is
- * {@code null} or blank, or {@code salesPrice} is {@code null},
- * negative, or zero
- */
- public Stock(String symbol, String company, BigDecimal salesPrice) {
- if (symbol == null || symbol.isBlank()) {
- throw new IllegalArgumentException("Symbol cannot be null or empty.");
- }
-
- if (company == null || company.isBlank()) {
- throw new IllegalArgumentException("Company cannot be null or empty.");
- }
-
- if (salesPrice == null || salesPrice.signum() <= 0) {
- throw new IllegalArgumentException("SalesPrice cannot be null, negative, or zero.");
- }
-
- this.symbol = symbol;
- this.company = company;
- this.prices = new ArrayList<>();
- this.prices.add(salesPrice);
- }
-
- /**
- * Returns the stock's unique ticker symbol.
- *
- * @return the stock's ticker symbol
- */
- public String getSymbol() {
- return this.symbol;
- }
-
- /**
- * Returns the full name of the company associated with the stock.
- *
- * @return the full name of the stock's company
- */
- public String getCompany() {
- return this.company;
- }
-
- /**
- * Returns the current sale price per share of the stock.
- *
- * The current price is defined as the most recent price in the price history.
- *
- * @return the current sale price of the stock
- */
- public BigDecimal getSalesPrice() {
- return this.prices.get(prices.size() - 1);
- }
-
- /**
- * Adds a new sales price to the stock's price history.
- *
- * The added price becomes the new current market price.
- *
- *
- *
- * The new price must be positive.
- *
- * @param price the new market price per share of the stock; must not be {@code null}, negative or zero
- * @throws IllegalArgumentException if {@code price} is {@code null}, negative, or zero
- */
- public void addNewSalesPrice(BigDecimal price) {
- if (price == null || price.signum() <= 0) {
- throw new IllegalArgumentException("Price cannot be null, negative, or zero.");
- }
- this.prices.add(price);
- }
-
- /**
- * Returns a list of all the sales prices for this stock since (and including) week 1.
- *
- * @return a list of all the stock's sales prices
- */
- public List getHistoricalPrices() {
- return this.prices;
- }
-
- /**
- * Returns the maximum historical sales price since (and including) week 1 for the stock.
- *
- * @return the stock's highest sales price
- */
- public BigDecimal getHighestPrice() {
- return Collections.max(this.prices);
- }
-
- /**
- * Returns the minimum historical sales price since (and including) week 1 for the stock.
- *
- * @return the stock's lowest sales price
- */
- public BigDecimal getLowestPrice() {
- return Collections.min(this.prices);
- }
-
- /**
- * Returns the sales price increase/decrease from previous week to this week. Returns 0 if
- * the price list only has 1 value.
- *
- * @return the net difference in sales price between the last and second last week
- */
- public BigDecimal getLatestPriceChange() {
- if (!(this.prices.size() < 2)) {
- return this.prices.getLast().subtract(this.prices.get(this.prices.size() - 2));
- }
- else {
- return BigDecimal.ZERO;
- }
- }
-}
diff --git a/millions/src/main/java/no/ntnu/gruppe53/Transaction.java b/millions/src/main/java/no/ntnu/gruppe53/Transaction.java
deleted file mode 100644
index 5ba478c..0000000
--- a/millions/src/main/java/no/ntnu/gruppe53/Transaction.java
+++ /dev/null
@@ -1,99 +0,0 @@
-package no.ntnu.gruppe53;
-
-/**
- * Represents a financial transaction of a {@link Share}.
- *
- * A transaction occurs in a specific week and uses a
- * {@link TransactionCalculator} to determine the monetary change.
- * Transactions are unique and is committed to a {@link Player}'s
- * {@link Portfolio}.
- *
- *
- *
- * Subclasses define the nature of the transaction (e.g. purchase
- * or sale) by implementing the {@link #commit(Player)}
- *
- */
-
-abstract public class Transaction {
- private final Share share;
- private final int week;
- private final TransactionCalculator calculator;
- protected boolean committed;
-
- /**
- * Constructor for a transaction.
- *
- * @param share the share for the transaction; must not be {@code null}
- * @param week the week the transaction takes place; must be greater than 0
- * @param calculator the transaction calculator used to
- * compute the {@link Transaction}
- * @throws IllegalArgumentException if {@code share} is {@code null} or {@code week}
- * is less than 1
- */
-
- protected Transaction(Share share, int week, TransactionCalculator calculator) {
- if (week <= 0) {
- throw new IllegalArgumentException("Week cannot be negative or zero");
- }
-
- this.share = share;
- this.week = week;
- this.calculator = calculator;
- this.committed = false;
- }
-
- /**
- * Returns the share for the current transaction.
- *
- * @return the share belonging to the transaction
- */
- public Share getShare() {
- return this.share;
- }
-
- /**
- * Returns the week the transaction took place.
- *
- * @return the week for the transaction
- */
- public int getWeek() {
- return this.week;
- }
-
- /**
- * Returns the transaction calculator used to calculate
- * the transaction.
- *
- * @return the transaction calculator used for the transaction
- */
-
- public TransactionCalculator getCalculator() {
- return this.calculator;
- }
-
- /**
- * Returns whether the transaction has been committed to a player.
- *
- * @return {@code true} if committed, {@code false} otherwise
- */
-
- public boolean isCommitted() {
- return this.committed;
- }
-
- /**
- * Commits the transaction to a player, making it unique.
- *
- *
- * Subclasses should define the business logic required before
- * committing. Then {@code super.commit(player)} should be called
- * once the transaction has been successfully applied.
- *
- * @param player the player performing the transaction; must not be {@code null}
- */
-
- public void commit(Player player) {
- this.committed = true;
- }
-}
diff --git a/millions/src/main/java/no/ntnu/gruppe53/TransactionArchive.java b/millions/src/main/java/no/ntnu/gruppe53/TransactionArchive.java
deleted file mode 100644
index a6ca20d..0000000
--- a/millions/src/main/java/no/ntnu/gruppe53/TransactionArchive.java
+++ /dev/null
@@ -1,136 +0,0 @@
-package no.ntnu.gruppe53;
-
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.List;
-
-/**
- * Maintains a history of all completed {@link Transaction}s.
- *
- * A {@code TransactionArchive} stores transactions in the order
- * they are added and provides methods to retrieve transactions
- * filtered by type and week.
- *
- */
-
-public class TransactionArchive {
- private final List transactions;
-
- /**
- * Creates an empty transaction archive.
- */
-
- public TransactionArchive() {
- this.transactions = new ArrayList<>();
- }
-
- /**
- * Adds a transaction to the archive.
- *
- * @param transaction the transaction being added; must not be {@code null}
- * @return {@code true} if the transaction was successfully added
- * {@code false} otherwise
- * @throws IllegalArgumentException if {@code transaction} is {@code null}
- */
-
- public boolean add(Transaction transaction) {
- if (transaction == null) {
- throw new IllegalArgumentException("Transaction cannot be null.");
- }
-
- this.transactions.add(transaction);
-
- return this.getTransactions().get(transactions.size()-1).equals(transaction);
- }
-
- /**
- * Checks whether there are no recorded transactions in the archive.
- *
- * @return {@code true} if the archive is empty.
- * {@code false} otherwise
- */
-
- public boolean isEmpty() {
- return this.transactions.isEmpty();
- }
-
- /**
- * Returns all the transactions contained in the archive.
- *
- * @return the list of transactions
- */
-
- public List getTransactions() {
- return this.transactions;
- }
-
- /**
- * Returns all {@link Purchase} transactions for the given week.
- *
- * @param week the week number for transactions; must be greater than 0
- * @return a list of purchase transactions for the given week;
- * empty if none exist
- * @throws IllegalArgumentException if {@code week} is not greater than 0
- */
-
- public List getPurchases(int week) {
- if (week <= 0) {
- throw new IllegalArgumentException("Week must be greater than 0.");
- }
- var purchaseList = new ArrayList();
-
- for (Transaction t : this.transactions) {
- if (t instanceof Purchase && t.getWeek() == week) {
- purchaseList.add((Purchase) t);
- }
- }
- return purchaseList;
- }
-
- /**
- * Returns all {@link Sale} transactions for the given week.
- *
- * @param week the week number for transactions; must be greater than 0
- * @return a list of sale transactions for the given week;
- * empty if none exist
- * @throws IllegalArgumentException if {@code week} is not greater than 0
- */
-
- public List getSales(int week) {
- if (week <= 0) {
- throw new IllegalArgumentException("Week must be greater than 0.");
- }
-
- var saleList = new ArrayList();
-
- for (Transaction t : this.transactions) {
- if (t instanceof Sale && t.getWeek() == week) {
- saleList.add((Sale) t);
- }
- }
- return saleList;
- }
-
- /**
- * Counts the number of distinct week numbers represented in the
- * transaction archive.
- *
- * A week is considered distinct if at least one transaction has
- * occurred within that week. Multiple transactions in the span of the
- * same week will only be counted as one distinct week.
- *
- *
- * @return the number of unique week values in the archive;
- * {@code 0} if the archive contains no transactions
- */
-
- public int countDistinctWeeks() {
- var distinctWeeks = new HashSet();
-
- for (Transaction t : this.transactions) {
- distinctWeeks.add(t.getWeek());
- }
-
- return distinctWeeks.size();
- }
-}
diff --git a/millions/src/main/java/no/ntnu/gruppe53/TransactionCalculator.java b/millions/src/main/java/no/ntnu/gruppe53/TransactionCalculator.java
deleted file mode 100644
index 0ad9274..0000000
--- a/millions/src/main/java/no/ntnu/gruppe53/TransactionCalculator.java
+++ /dev/null
@@ -1,11 +0,0 @@
-package no.ntnu.gruppe53;
-
-import java.math.BigDecimal;
-
-public interface TransactionCalculator {
- BigDecimal calculateGross();
- BigDecimal calculateCommission();
- BigDecimal calculateTax();
- BigDecimal calculateTotal();
-
-}
diff --git a/millions/src/main/java/no/ntnu/gruppe53/controller/GameController.java b/millions/src/main/java/no/ntnu/gruppe53/controller/GameController.java
new file mode 100644
index 0000000..0514822
--- /dev/null
+++ b/millions/src/main/java/no/ntnu/gruppe53/controller/GameController.java
@@ -0,0 +1,377 @@
+package no.ntnu.gruppe53.controller;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.List;
+import javafx.application.Platform;
+import javafx.scene.control.Alert;
+import javafx.scene.control.ButtonBar;
+import javafx.scene.control.ButtonType;
+import javafx.stage.Stage;
+import javafx.util.Subscription;
+import no.ntnu.gruppe53.model.Exchange;
+import no.ntnu.gruppe53.model.Player;
+import no.ntnu.gruppe53.model.PurchaseCalculator;
+import no.ntnu.gruppe53.model.SaleCalculator;
+import no.ntnu.gruppe53.model.Share;
+import no.ntnu.gruppe53.model.Stock;
+import no.ntnu.gruppe53.service.LanguageManager;
+import no.ntnu.gruppe53.service.StockLineChartService;
+import no.ntnu.gruppe53.view.EndView;
+import no.ntnu.gruppe53.view.FooterBar;
+import no.ntnu.gruppe53.view.MarketView;
+import no.ntnu.gruppe53.view.NavigationBar;
+import no.ntnu.gruppe53.view.PortfolioView;
+import no.ntnu.gruppe53.view.TransactionArchiveView;
+
+/**
+ * Represents the main controller for the views of the stock game.
+ *
+ *
Uses a {@link LanguageManager} for methods to change text dynamically.
+ */
+public class GameController {
+ private Player player;
+ private Exchange exchange;
+ private final LanguageManager lm = LanguageManager.getInstance();
+
+ private final ViewController vm;
+
+ private MarketView marketView;
+ private PortfolioView portfolioView;
+ private TransactionArchiveView transactionArchiveView;
+ private EndView endView;
+
+ private Subscription selectedStockPriceSubscription;
+ private Subscription selectedSharePriceSubscription;
+
+ /**
+ * Initializes the {@link ViewController} to be used for switching views.
+ *
+ * @param vm {@code ViewController} to be used when switching views
+ */
+ public GameController(ViewController vm) {
+ this.vm = vm;
+ }
+
+ /**
+ * Starts a new game by initializing all main views and navigation.
+ *
+ *
Creates the player and exchange to be used by the game from
+ * data from the {@link GameSetupController}.
+ */
+ public void startNewGame() {
+ Stage primaryStage = vm.getStage();
+
+ GameSetupController setupController = new GameSetupController(primaryStage);
+ GameSetupController.SetupResult setupResult = setupController.setupNewGame();
+
+ if (setupResult == null) {
+ return;
+ }
+
+ player = setupResult.getPlayer();
+ List stocks = setupResult.getStocks();
+
+ exchange = new Exchange("Millions Exchange", stocks);
+
+ NavigationBar sharedNav = new NavigationBar();
+ FooterBar sharedFooter = new FooterBar();
+ vm.setGlobalPanels(sharedNav, sharedFooter);
+
+ marketView = new MarketView();
+ // marketView.setPlayer(player);
+ marketView.setExchange(exchange);
+
+ portfolioView = new PortfolioView();
+ portfolioView.setPlayer(player);
+
+ transactionArchiveView = new TransactionArchiveView();
+ transactionArchiveView.setPlayer(player);
+
+ endView = new EndView();
+
+ sharedNav.bindPlayer(player);
+ sharedNav.bindExchange(exchange);
+ sharedFooter.setExchange(exchange);
+
+ vm.addView("portfolio", portfolioView);
+ vm.addView("market", marketView);
+ vm.addView("history", transactionArchiveView);
+ vm.addView("endGame", endView);
+
+ initialize(sharedNav, sharedFooter);
+
+ vm.switchView("market");
+ }
+
+ /**
+ * Switches the view to the {@link MarketView}.
+ *
+ *
MarketView lets the player perform {@link no.ntnu.gruppe53.model.Purchase} transactions.
+ */
+ public void showMarketView() {
+ if (marketView != null) {
+ vm.switchView("market");
+ }
+ }
+
+ /**
+ * Switches the view to the {@link PortfolioView}.
+ *
+ *
Contains the data from a player's {@link no.ntnu.gruppe53.model.Portfolio}.
+ *
+ *
PortfolioView lets theEndView player perform
+ * {@link no.ntnu.gruppe53.model.Sale} transactions.
+ */
+ public void showPortfolio() {
+ if (portfolioView != null) {
+ vm.switchView("portfolio");
+ }
+ }
+
+ /**
+ * Switches the view to the {@link TransactionArchiveView}.
+ *
+ *
TransactionArchiveView contains data from
+ * {@link no.ntnu.gruppe53.model.TransactionArchive}
+ */
+ public void showTransactionHistory() {
+ if (transactionArchiveView != null) {
+ vm.switchView("history");
+ }
+ }
+
+ /**
+ * Initializes the {@link NavigationBar} and {@link FooterBar} for the controller.
+ *
+ *
Binds functionality to exposed buttons and lists in views.
+ *
+ *
Sets subscriptions for observable values for views.
+ *
+ *
Sets the method for setting the selected language locale for the footer.
+ *
+ * @param stockSymbol the symbol/ticker of the stock to purchase shares for
+ * @param quantity the quantity of shares to be purchased
+ */
+ private void purchaseStock(String stockSymbol, int quantity) {
+ if (exchange == null || player == null || stockSymbol == null || quantity <= 0) {
+ return;
+ }
+
+ try {
+ exchange.buy(stockSymbol, BigDecimal.valueOf(quantity), player);
+ } catch (IllegalStateException | IllegalArgumentException ex) {
+ showErrorAlert("Purchase Failed", ex.getMessage());
+ }
+ }
+
+ /**
+ * Handles sale of the selected share.
+ */
+ private void handleSell() {
+ Share selectedShare = portfolioView.getSelectedShare();
+ if (selectedShare != null) {
+ sellShare(selectedShare);
+ }
+ }
+
+ /**
+ * Lets a player sell a share from their portfolio.
+ *
+ * @param selectedShare the currently selected share
+ */
+ private void sellShare(Share selectedShare) {
+ if (exchange == null || player == null || selectedShare == null) {
+ return;
+ }
+ try {
+ exchange.sell(selectedShare, player);
+ } catch (IllegalStateException | IllegalArgumentException ex) {
+ showErrorAlert("Sale Failed", ex.getMessage());
+ }
+ }
+
+ /**
+ * Creates a {@link PurchaseCalculator} that can be used for the
+ * setPurchaseCalculator() function in the {@link MarketView}.
+ *
+ * @param stock the currently selected stock
+ * @param quantity the currently selected quantity
+ * @return {@link PurchaseCalculator}
+ */
+ private PurchaseCalculator calculatePurchase(Stock stock, int quantity) {
+ stock = marketView.getSelectedStock();
+ quantity = marketView.getQuantityPurchase();
+
+ Share share = new Share(stock, BigDecimal.valueOf(quantity), stock.getSalesPrice());
+ PurchaseCalculator purchaseCalculator;
+ return purchaseCalculator = new PurchaseCalculator(share);
+ }
+
+ /**
+ * Creates a {@link SaleCalculator} that can be used for the
+ * setSaleCalculator() function in the {@link PortfolioView}.
+ *
+ * @param share the share to use the calculator on
+ * @return {@link SaleCalculator}
+ */
+ private SaleCalculator calculateSale(Share share) {
+
+ share = portfolioView.getSelectedShare();
+
+ SaleCalculator saleCalculator;
+ return saleCalculator = new SaleCalculator(share);
+ }
+
+ /**
+ * Creates an alert where the Player can confirm if they want to end the game.
+ * If yes, then sells entire portfolio and displays the EndView.
+ */
+ private void endGame() {
+ Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
+ alert.titleProperty().bind(lm.bindString("endGameAlertTitle"));
+ alert.headerTextProperty().bind(lm.bindString("endGameAlertHeaderText"));
+ alert.contentTextProperty().bind(lm.bindString("endGameAlertContentText"));
+
+ ButtonType yesButton = new ButtonType(lm.getString("yes"), ButtonBar.ButtonData.YES);
+ ButtonType noButton = new ButtonType(lm.getString("no"), ButtonBar.ButtonData.NO);
+
+ alert.getButtonTypes().setAll(yesButton, noButton);
+
+ alert.showAndWait().ifPresent(button -> {
+ if (button == yesButton) {
+ sellPortfolio();
+ vm.setGlobalPanels(null, null);
+ vm.switchView("endGame");
+
+ endView.getStatusLabel().setText(player.getStatus(exchange));
+ endView.getMoneyLabel().setText(
+ player.getMoney().setScale(2, RoundingMode.HALF_EVEN).toString());
+ }
+ });
+ }
+
+ /**
+ * Sells a player's entire portfolio.
+ */
+ private void sellPortfolio() {
+ List sharesToSell = List.copyOf(player.getPortfolio().getShares());
+
+ for (Share share : sharesToSell) {
+ exchange.sell(share, player);
+ }
+ }
+
+ /**
+ * Creates a default error alert to be used for notifying the player of errors.
+ *
+ * @param title title of the error alert
+ * @param content the message content of the error alert
+ */
+ private void showErrorAlert(String title, String content) {
+ Alert alert = new Alert(Alert.AlertType.ERROR);
+ alert.setTitle(title);
+ alert.setHeaderText(null);
+ alert.setContentText(content);
+ alert.showAndWait();
+ }
+}
diff --git a/millions/src/main/java/no/ntnu/gruppe53/controller/GameSetupController.java b/millions/src/main/java/no/ntnu/gruppe53/controller/GameSetupController.java
new file mode 100644
index 0000000..feced72
--- /dev/null
+++ b/millions/src/main/java/no/ntnu/gruppe53/controller/GameSetupController.java
@@ -0,0 +1,122 @@
+package no.ntnu.gruppe53.controller;
+
+import java.io.File;
+import java.math.BigDecimal;
+import java.util.List;
+import javafx.stage.Stage;
+import no.ntnu.gruppe53.model.Exchange;
+import no.ntnu.gruppe53.model.Player;
+import no.ntnu.gruppe53.model.Stock;
+import no.ntnu.gruppe53.service.FileHandler;
+import no.ntnu.gruppe53.view.CsvChooserView;
+import no.ntnu.gruppe53.view.PlayerNameChooserView;
+import no.ntnu.gruppe53.view.PlayerStartingMoneyChooserView;
+
+/**
+ * Controller for setting up relevant game data to start a new game.
+ *
+ *
Lets the {@link Player} set their name, starting money
+ * and provide a .csv file of {@link Stock} data to populate
+ * the {@link Exchange}.
+ */
+public class GameSetupController {
+ /**
+ * Nested class to pass the player and stocks values over to the
+ * {@link GameController}.
+ */
+ public static class SetupResult {
+ private final Player player;
+ private final List stocks;
+
+ /**
+ * Initializes the player and stock variables.
+ *
+ * @param player the player that will play the stock game
+ * @param stocks the list of stocks to populate the exchange
+ */
+ public SetupResult(Player player, List stocks) {
+ this.player = player;
+ this.stocks = stocks;
+ }
+
+ /**
+ * Returns the player object containing the set name and starting money.
+ *
+ * @return the player object for the player playing the game
+ */
+ public Player getPlayer() {
+ return player;
+ }
+
+ /**
+ * A list of stocks that will be used for buying and selling on the exchange.
+ *
+ * @return a list of stock objects
+ */
+ public List getStocks() {
+ return stocks;
+ }
+ }
+
+ private final Stage stage;
+
+ /**
+ * Initializes the initial stage for the view setting up the relevant game data.
+ *
+ * @param stage the initial stage to be used
+ */
+ public GameSetupController(Stage stage) {
+ this.stage = stage;
+ }
+
+ /**
+ * Sets up the initial data for the {@code Player} and {@code Exchange}.
+ *
+ *
Uses default values if the player does not enter their desired values:
+ *
+ *
Player name: Player
+ *
Player starting money: 5000
+ *
CSV file: resources/sp500.csv
+ *
+ *
+ * @return a SetupResult object containing data for the player and the exchange
+ */
+ public SetupResult setupNewGame() {
+ PlayerNameChooserView playerNameDialog = new PlayerNameChooserView();
+ // Player name
+ String playerName =
+ playerNameDialog.getDialog().orElse("Player");
+
+ if (playerName.isBlank()) {
+ playerName = "Player";
+ }
+
+ // Starting money
+ PlayerStartingMoneyChooserView playerStartingMoneyDialog = new PlayerStartingMoneyChooserView();
+
+ BigDecimal startingMoney =
+ playerStartingMoneyDialog.getDialog()
+ .orElse(new BigDecimal("5000"));
+
+
+ // .csv File
+ CsvChooserView csvChooser = new CsvChooserView();
+ File csvFile = csvChooser.getDialog(stage);
+
+ List stocks;
+
+ if (csvFile == null) {
+ stocks = FileHandler.readStocksFromFile("src/main/resources", "sp500.csv");
+ } else {
+ stocks = FileHandler.readStocksFromFile(csvFile.getParent(), csvFile.getName());
+
+ if (stocks.isEmpty()) {
+ stocks = FileHandler.readStocksFromFile("src/main/resources", "sp500.csv");
+ }
+ }
+
+ Player player = new Player(playerName, startingMoney);
+
+ return new SetupResult(player, stocks);
+ }
+}
\ No newline at end of file
diff --git a/millions/src/main/java/no/ntnu/gruppe53/controller/StartViewController.java b/millions/src/main/java/no/ntnu/gruppe53/controller/StartViewController.java
new file mode 100644
index 0000000..5b743a9
--- /dev/null
+++ b/millions/src/main/java/no/ntnu/gruppe53/controller/StartViewController.java
@@ -0,0 +1,58 @@
+package no.ntnu.gruppe53.controller;
+
+import java.util.List;
+import javafx.application.Platform;
+import no.ntnu.gruppe53.service.LanguageManager;
+import no.ntnu.gruppe53.service.LanguageOption;
+import no.ntnu.gruppe53.service.LanguageRegistry;
+import no.ntnu.gruppe53.view.StartView;
+
+/**
+ * The controller for {@link StartView},
+ * the first view for the player upon launching the app.
+ *
+ *
Contains three choices:
+ *
+ *
New Game
+ *
Language Select
+ *
Quit
+ *
+ *
+ *
Uses a {@link LanguageManager} to change the UI language.
+ */
+public class StartViewController {
+ /**
+ * Handles the events when "New Game" and "Quit" is pressed.
+ *
+ * @param view the StartView to be displayed
+ * @param vm the {@link ViewController} to handle switching of views
+ */
+ public StartViewController(StartView view, ViewController vm) {
+ view.setOnNewGame(() -> {
+ GameController gameController = new GameController(vm);
+ gameController.startNewGame();
+ });
+
+ view.setOnLanguage(() -> {
+ LanguageManager lm = LanguageManager.getInstance();
+ List availableLanguages = LanguageRegistry.getLanguages();
+
+ int currentIndex = 0;
+
+ for (int i = 0; i < availableLanguages.size(); i++) {
+ if (availableLanguages.get(i).locale().equals(lm.getLocale())) {
+ currentIndex = i;
+ break;
+ }
+ }
+
+ int nextIndex = (currentIndex + 1) % availableLanguages.size();
+ LanguageOption nextLanguage = availableLanguages.get(nextIndex);
+
+ lm.setLocale(nextLanguage.locale());
+ });
+
+
+ view.setOnQuit(Platform::exit);
+ }
+}
\ No newline at end of file
diff --git a/millions/src/main/java/no/ntnu/gruppe53/controller/ViewController.java b/millions/src/main/java/no/ntnu/gruppe53/controller/ViewController.java
new file mode 100644
index 0000000..f24cfeb
--- /dev/null
+++ b/millions/src/main/java/no/ntnu/gruppe53/controller/ViewController.java
@@ -0,0 +1,82 @@
+package no.ntnu.gruppe53.controller;
+
+import java.util.HashMap;
+import java.util.Map;
+import javafx.scene.Parent;
+import javafx.scene.Scene;
+import javafx.scene.layout.BorderPane;
+import javafx.stage.Stage;
+import no.ntnu.gruppe53.view.FooterBar;
+import no.ntnu.gruppe53.view.NavigationBar;
+
+/**
+ * Controller for setting and switching between views.
+ *
+ *
Uses a {@link HashMap} as an archive for registered views that
+ * are possible to switch between.
+ */
+public class ViewController {
+ private final Stage stage;
+ private final Map views = new HashMap<>();
+ private final BorderPane mainLayout;
+
+ /**
+ * Initializes a ViewController using the {@link Stage} as the first default view.
+ *
+ *
Uses a {@link BorderPane} layout for views.
+ */
+ public ViewController(Stage stage) {
+ this.stage = stage;
+ this.mainLayout = new BorderPane();
+ Scene scene = new Scene(mainLayout, 1200, 900);
+ this.stage.setScene(scene);
+ }
+
+ /**
+ * Returns the stage of the ViewController.
+ *
+ * @return the stage of the ViewController.
+ */
+ public Stage getStage() {
+ return this.stage;
+ }
+
+ /**
+ * Sets the top and bottom part of the BorderPane.
+ *
+ *
Uses {@link NavigationBar} as the top bar,
+ * and {@link FooterBar} as the bottom bar.
+ */
+ public void setGlobalPanels(NavigationBar navigationBar, FooterBar footerBar) {
+ this.mainLayout.setTop(navigationBar);
+ this.mainLayout.setBottom(footerBar);
+ }
+
+ /**
+ * Adds a view to the stored archive of possible views.
+ *
+ * @param name the name/key to refer to the view when switching view
+ * @param view the view to be added
+ */
+ public void addView(String name, Parent view) {
+ views.put(name, view);
+ }
+
+ /**
+ * Switches the to the given view based on the string key
+ * stored in the view archive.
+ *
+ *
Only switches the center pane.
+ *
+ * @param name the name/key of the view to switch to
+ */
+ public void switchView(String name) {
+ Parent view = views.get(name);
+
+ if (view == null) {
+ throw new IllegalArgumentException("No views found matching: " + name);
+ }
+
+ mainLayout.setCenter(view);
+ }
+}
diff --git a/millions/src/main/java/no/ntnu/gruppe53/model/Exchange.java b/millions/src/main/java/no/ntnu/gruppe53/model/Exchange.java
new file mode 100644
index 0000000..86fef14
--- /dev/null
+++ b/millions/src/main/java/no/ntnu/gruppe53/model/Exchange.java
@@ -0,0 +1,284 @@
+package no.ntnu.gruppe53.model;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.Collections;
+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;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+
+/**
+ * Represents an exchange/market containing {@link Stock}s and allows
+ * a {@link Player} to buy {@link Share}s of said stocks.
+ *
+ *
Contains method to advance the week of the market, finding stocks,
+ * gainers, and losers on the exchange.
+ */
+public class Exchange {
+ private final String name;
+ private int week;
+ private final Map stockMap;
+ private final Random random;
+ private final ObservableList stocks = FXCollections.observableArrayList();
+ private final ObjectProperty currentWeekProperty = new SimpleObjectProperty<>();
+ private final TransactionFactory purchaseFactory;
+ private final TransactionFactory saleFactory;
+
+ /**
+ * Sets the name of the exchange and the list of stocks to populate it.
+ *
+ *
Validates that the values are proper before creating the exchange.
+ *
+ *
Initializes factories for creating a {@link Transaction}.
+ *
+ * @param name the name of the exchange
+ * @param stocks the list of stocks to be tradeable on the exchange
+ * @throws IllegalArgumentException if exchange is null or blank, if list of stocks is null, or if
+ * the list of stocks contains a null or duplicate symbol stock
+ */
+ public Exchange(String name, List stocks) {
+ if (name == null || name.isBlank()) {
+ throw new IllegalArgumentException("Exchange name cannot be null or blank");
+ }
+
+ if (stocks == null) {
+ throw new IllegalArgumentException("Stock list cannot be null");
+ }
+
+ this.name = name;
+ this.week = 1;
+ this.currentWeekProperty.set(week);
+ this.stockMap = new HashMap<>();
+ this.random = new Random();
+
+ for (Stock stock : stocks) {
+ if (stock == null) {
+ throw new IllegalArgumentException("Stock cannot be null");
+ }
+
+ if (stockMap.containsKey(stock.getSymbol())) {
+ throw new IllegalArgumentException("Duplicate stock symbol: " + stock.getSymbol());
+ }
+
+ stockMap.put(stock.getSymbol(), stock);
+ this.stocks.add(stock);
+ }
+
+ this.purchaseFactory = new PurchaseFactory();
+ this.saleFactory = new SaleFactory();
+ }
+
+ /**
+ * Returns the name of the exchange.
+ *
+ * @return the name of the exchange
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Returns the current week of the exchange.
+ *
+ * @return the current week of the exchange
+ */
+ public int getWeek() {
+ return week;
+ }
+
+ /**
+ * Checks whether a stock with the given symbol/ticker exists in the exchange.
+ *
+ *
Returns {@code true} if it does,v{@code false} otherwise.
+ *
+ * @param symbol the symbol/ticker of the stock
+ * @return {@code true} if the exchange contains the stock, otherwise {@code false}
+ */
+ public boolean hasStock(String symbol) {
+ return stockMap.containsKey(symbol);
+ }
+
+ /**
+ * Returns the stock object matching the given symbol/ticker.
+ *
+ * @param symbol the stock symbol/ticker
+ * @return the stock object matching the symbol/ticker
+ * @throws IllegalArgumentException if search symbol is null or symbol is not in stockMap
+ */
+ public Stock getStock(String symbol) {
+ if (symbol == null || !stockMap.containsKey(symbol)) {
+ throw new IllegalArgumentException("Stock not found: " + symbol);
+ }
+ return stockMap.get(symbol);
+ }
+
+ /**
+ * Returns an {@link ObservableList} of the list of stocks for JavaFX to observe.
+ *
+ * @return an observable list of the stocks in the exchange
+ */
+ public ObservableList getObservableStocks() {
+ return stocks;
+ }
+
+ /**
+ * Returns a list of stocks matching the search term in either the symbol/ticker or company name.
+ *
+ * @param searchTerm the string to search for
+ * @return a list of stocks matching the search term
+ */
+ public List findStocks(String searchTerm) {
+ if (searchTerm == null || searchTerm.isBlank()) {
+ return Collections.emptyList();
+ }
+
+ String search = searchTerm.toLowerCase();
+ return stockMap.values().stream()
+ .filter(stock -> stock.getSymbol().toLowerCase().contains(search)
+ ||
+ stock.getCompany().toLowerCase().contains(search))
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * Represents a player buying a share on the exchange.
+ *
+ *
Creates a {@link Purchase} transaction for the
+ * purchase.
+ *
+ * @param symbol the symbol/ticker of the stock
+ * @param quantity the quantity of shares to buy for said stock
+ * @param player the player making the purchase
+ * @return a purchase transaction containing all details of the purchase
+ * @throws IllegalArgumentException if player or quantity is null
+ */
+ public Transaction buy(String symbol, BigDecimal quantity, Player player) {
+ if (player == null) {
+ throw new IllegalArgumentException("Player cannot be null");
+ }
+
+ if (quantity == null || quantity.compareTo(BigDecimal.ZERO) <= 0) {
+ throw new IllegalArgumentException("Quantity must be positive");
+ }
+
+ Stock stock = getStock(symbol);
+ Share share = new Share(stock, quantity, stock.getSalesPrice());
+ Transaction transaction = purchaseFactory.create(share, week);
+ transaction.commit(player);
+
+ return transaction;
+ }
+
+ /**
+ * Represents a player selling a share on the exchange.
+ *
+ *
Creates a {@link Sale} transaction for the sale.
+ *
+ * @param share the share to be sold
+ * @param player the player selling the share
+ * @return a sale transaction containing all details of the sale
+ * @throws IllegalArgumentException if player or share is null
+ */
+ public Transaction sell(Share share, Player player) {
+ if (player == null) {
+ throw new IllegalArgumentException("Player cannot be null");
+ }
+
+ if (share == null) {
+ throw new IllegalArgumentException("Share cannot be null");
+ }
+
+ Transaction transaction = saleFactory.create(share, week);
+ transaction.commit(player);
+ return transaction;
+ }
+
+ /**
+ * Advances the timeline of the exchange by 1 week.
+ *
+ *
Uses a randomized value for the increase and decrease
+ * of a stocks sales price, simulating market fluctuations on a weekly basis.
+ */
+ public void advance() {
+ this.week++;
+ this.currentWeekProperty.set(week);
+
+ double negativePriceChange = -0.05;
+ double positivePriceChange = 0.1;
+
+ for (Stock stock : stockMap.values()) {
+ BigDecimal currentPrice = stock.getSalesPrice();
+ double randomPriceMultiplier = negativePriceChange
+ + (positivePriceChange * random.nextDouble());
+ BigDecimal bdRandomPriceMultiplier = BigDecimal.valueOf(1 + randomPriceMultiplier);
+
+ BigDecimal changedPrice = currentPrice.multiply(bdRandomPriceMultiplier)
+ .setScale(2, RoundingMode.HALF_UP);
+
+ if (changedPrice.compareTo(BigDecimal.ZERO) <= 0) {
+ changedPrice = new BigDecimal(1);
+ }
+
+ stock.addNewSalesPrice(changedPrice);
+ }
+ }
+
+ /**
+ * Sorts the stocks in descending order and returns a list of stocks.
+ *
+ * @param entries amount of entries in the list to display
+ * @param comparator the comparator used to compare the stocks
+ * @return a list of stocks
+ * @throws IllegalArgumentException if the amount of entries is negative
+ */
+ private List getTopStocks(int entries, Comparator comparator) {
+ if (entries < 0) {
+ throw new IllegalArgumentException("Amount of entries in list cannot be negative.");
+ }
+
+ return stockMap.values().stream()
+ .sorted(comparator)
+ .limit(entries)
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * Lists the given amount of stocks sorted by highest increase in sales price since last week.
+ *
+ * @param entries amount of stocks to be shown in list
+ * @return a descending list of stocks with the highest price increase since last week
+ * @throws IllegalArgumentException if number of entries in list is negative
+ */
+ public List getGainers(int entries) {
+ return getTopStocks(entries, Comparator.comparing(Stock::getLatestPriceChange).reversed());
+ }
+
+ /**
+ * Lists the given amount of stocks sorted by the stocks with the lowest price increase (or
+ * highest price decrease).
+ *
+ * @param entries amount of stocks to be shown in the list
+ * @return a descending list of stocks with the highest price decrease or
+ * lowest price increase
+ * @throws IllegalArgumentException if number of entries in list is negative
+ */
+ public List getLosers(int entries) {
+ return getTopStocks(entries, Comparator.comparing(Stock::getLatestPriceChange));
+ }
+
+ /**
+ * Returns an observable of the current week.
+ *
+ * @return an observable of the current week
+ */
+ public ObjectProperty getCurrentWeekProperty() {
+ return currentWeekProperty;
+ }
+}
\ No newline at end of file
diff --git a/millions/src/main/java/no/ntnu/gruppe53/model/Player.java b/millions/src/main/java/no/ntnu/gruppe53/model/Player.java
new file mode 100644
index 0000000..6489d53
--- /dev/null
+++ b/millions/src/main/java/no/ntnu/gruppe53/model/Player.java
@@ -0,0 +1,204 @@
+package no.ntnu.gruppe53.model;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+
+/**
+ * Represents a participant in the trading game.
+ *
+ *
A {@code Player} has a name, a monetary balance, a {@link Portfolio}
+ * of owned {@link Share}s, and a {@link TransactionArchive} containing
+ * a history of the player's transactions.
+ *
+ *
The player starts with an initial amount of money and may gain or
+ * lose funds through transactions.
+ */
+public class Player {
+ private final String name;
+ private final BigDecimal startingMoney;
+ private BigDecimal money;
+ private final Portfolio portfolio;
+ private final TransactionArchive transactionArchive;
+
+ private final ObjectProperty moneyProperty = new SimpleObjectProperty<>();
+ private final ObjectProperty netWorthProperty = new SimpleObjectProperty<>();
+
+ /**
+ * Creates a player with a given starting balance.
+ *
+ *
Subscribes to the netWorth property to notify of changes in the player's net worth.
+ *
+ * @param name the player's name; must not be {@code null} or blank
+ * @param startingMoney the initial monetary balance of the
+ * player; must not be {@code null} or negative
+ * @throws IllegalArgumentException if {@code name} is {@code null} or
+ * blank, or if {@code startingMoney}
+ * is {@code null}, negative, or 0
+ */
+ public Player(String name, BigDecimal startingMoney) {
+ if (name == null || name.isBlank()) {
+ throw new IllegalArgumentException("Player name must not be null or empty.");
+ }
+
+ if (startingMoney == null || startingMoney.signum() <= 0) {
+ throw new IllegalArgumentException("Starting money must not be null, negative, nor 0.");
+ }
+
+ this.name = name;
+ this.startingMoney = startingMoney;
+ this.money = startingMoney;
+ this.moneyProperty.set(startingMoney);
+ this.portfolio = new Portfolio();
+ this.transactionArchive = new TransactionArchive();
+
+ portfolio.netWorthProperty().subscribe(price -> updateNetWorth());
+ updateNetWorth();
+ }
+
+ /**
+ * Returns an {@link ObjectProperty} of the player's net worth.
+ *
+ * @return an observable of the player's net worth
+ */
+ public ObjectProperty getNetWorthProperty() {
+ return netWorthProperty;
+ }
+
+ /**
+ * Mirrors the change in the player's net worth to the observable property.
+ */
+ private void updateNetWorth() {
+ netWorthProperty.set(getNetWorth());
+ }
+
+ /**
+ * Returns the full name of the player.
+ *
+ * @return the player's name
+ */
+ public String getName() {
+ return this.name;
+ }
+
+ /**
+ * Returns the player's current monetary balance.
+ *
+ * @return the player's current balance.
+ */
+ public BigDecimal getMoney() {
+ return this.money;
+ }
+
+ /**
+ * Increases the players monetary balance by the given amount.
+ *
+ *
Money added must be a {@code positive} number.
+ *
+ * @param amount the amount to add; must be positive and not {@code null}
+ * @throws IllegalArgumentException if {@code amount} is {@code null}
+ * or not positive
+ */
+ public void addMoney(BigDecimal amount) {
+ if (amount == null) {
+ throw new IllegalArgumentException("Amount must not be null.");
+ }
+
+ if (amount.signum() <= 0) {
+ throw new IllegalArgumentException("Amount must be positive.");
+ }
+
+ this.money = this.money.add(amount);
+ this.moneyProperty.set(this.money);
+ updateNetWorth();
+ }
+
+ /**
+ * Decreases the player's monetary balance by the given amount.
+ *
+ *
The player's monetary balance must not decrease below {@code 0}
+ * (overdrawn not possible).
+ *
+ * @param amount the amount to subtract; must not be negative or {@code null}
+ * @throws IllegalArgumentException if the amount is not positive,
+ * {@code null}, or insufficient funds are available
+ */
+ public void withdrawMoney(BigDecimal amount) {
+ if (amount == null) {
+ throw new IllegalArgumentException("Amount must not be null.");
+ }
+
+ if (amount.signum() <= 0) {
+ throw new IllegalArgumentException("Amount must be positive.");
+ }
+
+ if (this.money.subtract(amount).signum() < 0) {
+ throw new IllegalArgumentException("Insufficient funds.");
+ }
+
+ this.money = this.money.subtract(amount);
+ this.moneyProperty.set(this.money);
+ updateNetWorth();
+ }
+
+ /**
+ * Returns an observable of the player's current monetary funds.
+ *
+ * @return an observable of the player's current money
+ */
+ public ObjectProperty getMoneyProperty() {
+ return moneyProperty;
+ }
+
+ /**
+ * Return's the player's portfolio of shares.
+ *
+ * @return the player's portfolio
+ */
+ public Portfolio getPortfolio() {
+ return this.portfolio;
+ }
+
+ /**
+ * Returns the archive of the player's transaction history.
+ *
+ * @return the player's transaction archive
+ */
+ public TransactionArchive getTransactionArchive() {
+ return this.transactionArchive;
+ }
+
+ /**
+ * Returns the player's current net worth.
+ *
+ *
The net worth is the sum of the player's current money and
+ * all the value of their shares in their portfolio.
+ *
+ * @return the player's current net worth
+ */
+ public BigDecimal getNetWorth() {
+ return money.add(portfolio.getNetWorth());
+ }
+
+ /**
+ * Returns the current status/title of the player, representing their achievements.
+ *
+ * @param exchange the exchange the player is trading at
+ * @return a string representing the players current status
+ */
+ public String getStatus(Exchange exchange) {
+ String status = "Novice";
+ BigDecimal ratio = getNetWorth().divide(this.startingMoney, RoundingMode.HALF_EVEN);
+
+ if (ratio.compareTo(new BigDecimal("1.20")) >= 0 && exchange.getWeek() >= 10) {
+ status = "Investor";
+ }
+
+ if (ratio.compareTo(new BigDecimal("2")) >= 0 && exchange.getWeek() >= 20) {
+ status = "Speculator";
+ }
+
+ return status;
+ }
+}
\ No newline at end of file
diff --git a/millions/src/main/java/no/ntnu/gruppe53/model/Portfolio.java b/millions/src/main/java/no/ntnu/gruppe53/model/Portfolio.java
new file mode 100644
index 0000000..31fda6d
--- /dev/null
+++ b/millions/src/main/java/no/ntnu/gruppe53/model/Portfolio.java
@@ -0,0 +1,162 @@
+package no.ntnu.gruppe53.model;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.List;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+
+/**
+ * Represents the portfolio of the Player, containing information
+ * about which {@link Share}s are owned.
+ *
+ *
A {@code Portfolio} manages a mutable collection of shares, allowing
+ * shares to be added, removed, and queried.
+ *
+ *
Shares are associated with a unique Transaction, however, multiple
+ * shares from the same stock can exist in the portfolio.
+ */
+public class Portfolio {
+ private final ObservableList shares;
+ private final ObjectProperty netWorthProperty =
+ new SimpleObjectProperty<>(BigDecimal.ZERO);
+
+ /**
+ * Creates an empty portfolio for the player.
+ */
+ public Portfolio() {
+ this.shares = FXCollections.observableArrayList();
+ }
+
+ /**
+ * Adds a share to the player's portfolio.
+ *
+ *
The {@link Stock} must not be {@code null} and the share quantity
+ * and purchase price must be above zero.
+ *
+ * @param share the share owned by the player; must not be {@code null}
+ * @return {@code true} if the share was added successfully,
+ * {@code false} otherwise
+ * @throws IllegalArgumentException if {@code share} is {@code null}
+ */
+ public boolean addShare(Share share) {
+ if (share == null) {
+ throw new IllegalArgumentException("Share cannot be null");
+ }
+
+ this.shares.add(share);
+
+ share.getStock().salesPriceProperty()
+ .subscribe(price -> updateNetWorth());
+
+ updateNetWorth();
+
+ return true;
+ }
+
+ /**
+ * Removes a share from the player's portfolio.
+ *
+ *
The list must already exist in the portfolio to be successfully
+ * removed.
+ *
+ * @param share the share being removed; must not be {@code null}
+ * @return {@code true} if the share removal was successful,
+ * {@code false} otherwise.
+ * @throws IllegalArgumentException if {@code share} is {@code null}
+ */
+ public boolean removeShare(Share share) {
+ if (share == null) {
+ throw new IllegalArgumentException("Share cannot be null");
+ }
+
+ boolean removed = this.shares.removeIf(s -> s.equals(share));
+
+ if (removed) {
+ updateNetWorth();
+ }
+
+ return removed;
+ }
+
+ /**
+ * Returns a list of the shares currently in the player's portfolio.
+ *
+ * @return a list of the shares in this portfolio
+ */
+ public ObservableList getShares() {
+ return this.shares;
+ }
+
+ /**
+ * Returns a list of the shares that belongs to the stock with the
+ * given ticker symbol.
+ *
+ * @param symbol the ticker symbol of the stock to search for; must not be
+ * {@code null} or blank
+ * @return a list of the player's shares in the stock with the given
+ * stock symbol; an empty list if none are found
+ * @throws IllegalArgumentException if {@code symbol} is {@code null} or blank
+ */
+ public List getShares(String symbol) {
+ if (symbol == null || symbol.isBlank()) {
+ throw new IllegalArgumentException("Symbol cannot be null or blank");
+ }
+
+ var tempSymbols = new ArrayList();
+ for (Share s : this.shares) {
+
+ if (s.getStock().getSymbol().equals(symbol)) {
+ tempSymbols.add(s);
+ }
+ }
+
+ return tempSymbols;
+ }
+
+ /**
+ * Determines whether the player's portfolio contains the specified
+ * share.
+ *
+ * @param share the share to search for in this portfolio
+ * @return {@code true} if the player's portfolio contains the share
+ * {@code false} otherwise
+ */
+ public boolean contains(Share share) {
+ return shares.stream().anyMatch(s -> s.equals(share));
+ }
+
+ /**
+ * Calculates the net total value of the shares in the portfolio.
+ *
+ *
Portfolio value is based on current market price, not transaction rules.
+ *
+ * @return the total market value of the portfolio
+ */
+ public BigDecimal getNetWorth() {
+ return this.shares.stream()
+ .map(share ->
+ share.getStock().getSalesPrice()
+ .multiply(share.getQuantity())
+ )
+ .reduce(BigDecimal.ZERO, BigDecimal::add);
+ }
+
+ /**
+ * Returns an {@link ObjectProperty} of the net worth of the portfolio.
+ *
+ * @return an observable of the portfolio's net worth
+ */
+ public ObjectProperty netWorthProperty() {
+ return netWorthProperty;
+ }
+
+ /**
+ * Mirrors the portfolio's net worth to the observable.
+ */
+ private void updateNetWorth() {
+ netWorthProperty.set(getNetWorth());
+ }
+}
\ No newline at end of file
diff --git a/millions/src/main/java/no/ntnu/gruppe53/model/Purchase.java b/millions/src/main/java/no/ntnu/gruppe53/model/Purchase.java
new file mode 100644
index 0000000..ddb3ada
--- /dev/null
+++ b/millions/src/main/java/no/ntnu/gruppe53/model/Purchase.java
@@ -0,0 +1,63 @@
+package no.ntnu.gruppe53.model;
+
+import java.math.BigDecimal;
+
+/**
+ * Represents a purchase {@link Transaction} where a {@link Player} purchases
+ * a {@link Share}.
+ *
+ *
A purchase can only be commited if:
+ *
+ *
The player has sufficient funds.
+ *
The transaction has not already been committed.
+ *
+ *
+ */
+
+public class Purchase extends Transaction {
+
+ /**
+ * Constructs a purchase transaction for the given share and week.
+ *
+ * @param share the share to be purchased; must not be {@code null}
+ * @param week the week when the purchase transaction occurs; must be 1 or greater
+ */
+
+ public Purchase(Share share, int week) {
+ super(share, week, new PurchaseCalculator(share));
+ }
+
+ /**
+ * Commits the purchase to the given player, making the
+ * transaction unique.
+ *
+ *
If the transaction is not already committed, and the player
+ * has sufficient funds, the transaction will:
+ *
+ *
Withdraw money from the player
+ *
Add the share to the player's {@link Portfolio}
+ *
Archive the transaction in the player's
+ * {@link TransactionArchive}
+ *
+ *
+ *
+ * @param player the player performing the transaction; must not be {@code null}
+ * @throws IllegalArgumentException if {@code player} is {@code null}
+ */
+
+ @Override
+ public void commit(Player player) {
+ if (player == null) {
+ throw new IllegalArgumentException("Player cannot be null.");
+ }
+
+ if (!this.isCommitted() && ((player.getMoney().subtract(this.getCalculator().calculateTotal()))
+ .compareTo(BigDecimal.ZERO) >= 0)) {
+
+ player.withdrawMoney(this.getCalculator().calculateTotal());
+ player.getPortfolio().addShare(this.getShare());
+ player.getTransactionArchive().add(this);
+ super.commit(player);
+ }
+ }
+}
\ No newline at end of file
diff --git a/millions/src/main/java/no/ntnu/gruppe53/model/PurchaseCalculator.java b/millions/src/main/java/no/ntnu/gruppe53/model/PurchaseCalculator.java
new file mode 100644
index 0000000..08865b9
--- /dev/null
+++ b/millions/src/main/java/no/ntnu/gruppe53/model/PurchaseCalculator.java
@@ -0,0 +1,110 @@
+package no.ntnu.gruppe53.model;
+
+import java.math.BigDecimal;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+
+/**
+ * Calculates the costs associated with a {@link Purchase} of a given
+ * amount of {@link Share}s on the {@link Exchange}.
+ */
+public class PurchaseCalculator implements TransactionCalculator {
+ private final BigDecimal purchasePrice;
+ private final BigDecimal quantity;
+
+ // Objectproperties
+ private final ObjectProperty grossProperty = new SimpleObjectProperty<>();
+ private final ObjectProperty commissionProperty = new SimpleObjectProperty<>();
+ private final ObjectProperty totalProperty = new SimpleObjectProperty<>();
+
+ /**
+ * Sets the purchase price and quantity based on data from the given share.
+ *
+ *
Also sets up the ObjectProperties of the different calculations such
+ * that Subscriptions can listen to the calculations
+ *
+ * @param share the share to calculate purchase costs for
+ */
+ public PurchaseCalculator(Share share) {
+ this.purchasePrice = share.getPurchasePrice();
+ this.quantity = share.getQuantity();
+
+ this.grossProperty.set(calculateGross());
+ this.commissionProperty.set(calculateCommission());
+ this.totalProperty.set(calculateTotal());
+
+ }
+
+ /**
+ * Calculates the gross purchase price of the given share.
+ *
+ * @return the gross associated cost of purchasing the share
+ */
+ @Override
+ public BigDecimal calculateGross() {
+ return this.quantity.multiply(this.purchasePrice);
+ }
+
+ /**
+ * Calculates the commission of the exchange for purchasing a share.
+ *
+ *
Commission is set to a fixed 0.5% of gross purchase price.
+ *
+ * @return the commission taken by the exchange
+ */
+ @Override
+ public BigDecimal calculateCommission() {
+ BigDecimal commission = new BigDecimal("0.005");
+
+ return calculateGross().multiply(commission);
+ }
+
+ /**
+ * Calculates the tax for a purchase.
+ *
+ *
For a purchase, there is no tax.
+ *
+ * @return the tax for a purchase, in this case zero
+ */
+ @Override
+ public BigDecimal calculateTax() {
+ return new BigDecimal(0);
+ }
+
+ /**
+ * Calculates the total costs associated with the purchase of a share.
+ *
+ * @return the net total cost of purchasing a share
+ */
+ @Override
+ public BigDecimal calculateTotal() {
+ return calculateGross().add(calculateCommission()).add(calculateTax());
+ }
+
+ /**
+ * Returns an observable of the calculated gross.
+ *
+ * @return an observable of the calculated gross
+ */
+ public ObjectProperty getGrossProperty() {
+ return grossProperty;
+ }
+
+ /**
+ * Returns an observable of the calculated commission.
+ *
+ * @return an observable of the calculated commission
+ */
+ public ObjectProperty getCommissionProperty() {
+ return commissionProperty;
+ }
+
+ /**
+ * Returns an observable of the calculated total.
+ *
+ * @return an observable of the calculated total
+ */
+ public ObjectProperty getTotalProperty() {
+ return totalProperty;
+ }
+}
diff --git a/millions/src/main/java/no/ntnu/gruppe53/model/PurchaseFactory.java b/millions/src/main/java/no/ntnu/gruppe53/model/PurchaseFactory.java
new file mode 100644
index 0000000..a05169f
--- /dev/null
+++ b/millions/src/main/java/no/ntnu/gruppe53/model/PurchaseFactory.java
@@ -0,0 +1,19 @@
+package no.ntnu.gruppe53.model;
+
+/**
+ * Creates a {@link Purchase} {@link Transaction} of a {@link Share}.
+ */
+public class PurchaseFactory extends TransactionFactory {
+
+ /**
+ * Returns the purchase transaction for the given share and week.
+ *
+ * @param share the share to be purchased
+ * @param week the week of purchase
+ * @return the purchase transaction
+ */
+ @Override
+ protected Transaction createTransaction(Share share, int week) {
+ return new Purchase(share, week);
+ }
+}
diff --git a/millions/src/main/java/no/ntnu/gruppe53/model/Sale.java b/millions/src/main/java/no/ntnu/gruppe53/model/Sale.java
new file mode 100644
index 0000000..ff418ee
--- /dev/null
+++ b/millions/src/main/java/no/ntnu/gruppe53/model/Sale.java
@@ -0,0 +1,65 @@
+package no.ntnu.gruppe53.model;
+
+/**
+ * Represents a sale {@link Transaction} where a {@link Player} sells
+ * a {@link Share}.
+ *
+ *
A sale can only be commited if:
+ *
+ *
The share is in the players {@link Portfolio}
+ *
The transaction has not already committed
+ *
+ *
+ */
+
+public class Sale extends Transaction {
+
+ /**
+ * Constructs a sale transaction for the given share and week.
+ *
+ * @param share the share to be sold; must not be {@code null}
+ * @param week the week when the sale transaction occurs; must be greater than 0
+ */
+
+ public Sale(Share share, int week) {
+ super(share, week, new SaleCalculator(share));
+ }
+
+ /**
+ * Commits the sale to the given player, making the
+ * transaction unique.
+ *
+ *
If the player ows the share, the transaction will:
+ *
+ *
Add money to the player
+ *
Remove the share from the player's portfolio
+ *
Archive the transaction in the player's {@link TransactionArchive}
+ *
+ *
+ *
+ * @param player the player performing the transaction; must not be {@code null}
+ * @throws IllegalArgumentException if {@code player} is {@code null}
+ * @throws IllegalStateException if trying to sell a share not in the player's portfolio or
+ * if transactions has already been committed.
+ */
+
+ @Override
+ public void commit(Player player) {
+ if (player == null) {
+ throw new IllegalArgumentException("Player cannot be null");
+ }
+
+ if (this.isCommitted()) {
+ throw new IllegalStateException("Transaction has already been committed.");
+ }
+
+ if (!player.getPortfolio().contains(this.getShare())) {
+ throw new IllegalStateException("You do not own this share in your portfolio.");
+ }
+
+ player.addMoney(this.getCalculator().calculateTotal());
+ player.getPortfolio().removeShare(this.getShare());
+ player.getTransactionArchive().add(this);
+ super.commit(player);
+ }
+}
\ No newline at end of file
diff --git a/millions/src/main/java/no/ntnu/gruppe53/model/SaleCalculator.java b/millions/src/main/java/no/ntnu/gruppe53/model/SaleCalculator.java
new file mode 100644
index 0000000..a108d86
--- /dev/null
+++ b/millions/src/main/java/no/ntnu/gruppe53/model/SaleCalculator.java
@@ -0,0 +1,153 @@
+package no.ntnu.gruppe53.model;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+
+/**
+ * Calculates the costs associated with the {@link Sale} of a {@link Share} on the {@link Exchange},
+ * and the net total gain of selling said share.
+ */
+public class SaleCalculator implements TransactionCalculator {
+ private final BigDecimal purchasePrice;
+ private final BigDecimal salesPrice;
+ private final BigDecimal quantity;
+
+ // Objectproperties
+ private final ObjectProperty grossProperty = new SimpleObjectProperty<>();
+ private final ObjectProperty commissionProperty = new SimpleObjectProperty<>();
+ private final ObjectProperty taxProperty = new SimpleObjectProperty<>();
+ private final ObjectProperty totalProperty = new SimpleObjectProperty<>();
+ private final ObjectProperty profitProperty = new SimpleObjectProperty<>();
+
+ /**
+ * Sets the quantity, purchase and sales price based on the given share.
+ *
+ * @param share the share to calculate the costs and net total of a sale
+ */
+ public SaleCalculator(Share share) {
+ this.purchasePrice = share.getPurchasePrice();
+ this.quantity = share.getQuantity();
+ this.salesPrice = share.getStock().getSalesPrice();
+
+ this.grossProperty.set(calculateGross());
+ this.commissionProperty.set(calculateCommission());
+ this.taxProperty.set(calculateTax());
+ this.totalProperty.set(calculateTotal());
+ this.profitProperty.set(calculateTotal().subtract(purchasePrice.multiply(quantity)));
+ }
+
+ /**
+ * Calculates the gross sales price of the given share.
+ *
+ * @return the gross associated gain from selling the share
+ */
+ @Override
+ public BigDecimal calculateGross() {
+ return quantity.multiply(salesPrice)
+ .setScale(2, RoundingMode.HALF_UP);
+ }
+
+ /**
+ * Calculates the exchange's commission for selling the share.
+ *
+ *
The commission is set to a fixed 1% of the gross total sale value.
+ *
+ * @return the commission taken by the exchange
+ */
+ @Override
+ public BigDecimal calculateCommission() {
+
+ BigDecimal commissionRate = new BigDecimal("0.01");
+
+ return calculateGross()
+ .multiply(commissionRate)
+ .setScale(2, RoundingMode.HALF_UP);
+ }
+
+ /**
+ * Calculates the taxable profit from the sale.
+ *
+ *
Only calculates tax for profitable sales.
>
+ *
+ * @return the tax amount
+ */
+ @Override
+ public BigDecimal calculateTax() {
+
+ BigDecimal taxRate = new BigDecimal("0.30");
+
+ BigDecimal originalCost = purchasePrice.multiply(quantity);
+
+ BigDecimal profit = calculateGross()
+ .subtract(calculateCommission())
+ .subtract(originalCost);
+
+ if (profit.compareTo(BigDecimal.ZERO) <= 0) {
+ return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
+ }
+
+ return profit.multiply(taxRate)
+ .setScale(2, RoundingMode.HALF_UP);
+ }
+
+ /**
+ * Returns the net total gain for selling the share.
+ *
+ * @return the net total winnings of selling the share
+ */
+ @Override
+ public BigDecimal calculateTotal() {
+
+ return calculateGross()
+ .subtract(calculateCommission())
+ .subtract(calculateTax())
+ .setScale(2, RoundingMode.HALF_UP);
+ }
+
+ /**
+ * Returns an observable of the calculated gross.
+ *
+ * @return an observable of the calculated gross
+ */
+ public ObjectProperty getGrossProperty() {
+ return grossProperty;
+ }
+
+ /**
+ * Returns an observable of the calculated commission.
+ *
+ * @return an observable of the calculated commission
+ */
+ public ObjectProperty getCommissionProperty() {
+ return commissionProperty;
+ }
+
+ /**
+ * Returns an observable of the calculated tax.
+ *
+ * @return an observable of the calculated tax
+ */
+ public ObjectProperty getTaxProperty() {
+ return taxProperty;
+ }
+
+ /**
+ * Returns an observable of the calculated total.
+ *
+ * @return an observable of the calculated total
+ */
+ public ObjectProperty getTotalProperty() {
+ return totalProperty;
+ }
+
+ /**
+ * Returns an observable of the calculated profit.
+ *
+ * @return an observable of the calculated profit
+ */
+ public ObjectProperty getProfitProperty() {
+ return profitProperty;
+ }
+}
\ No newline at end of file
diff --git a/millions/src/main/java/no/ntnu/gruppe53/model/SaleFactory.java b/millions/src/main/java/no/ntnu/gruppe53/model/SaleFactory.java
new file mode 100644
index 0000000..9128a74
--- /dev/null
+++ b/millions/src/main/java/no/ntnu/gruppe53/model/SaleFactory.java
@@ -0,0 +1,19 @@
+package no.ntnu.gruppe53.model;
+
+/**
+ * Creates a {@link Sale} {@link Transaction} of a {@link Share}.
+ */
+public class SaleFactory extends TransactionFactory {
+
+ /**
+ * Returns the sale transaction of the given share and week.
+ *
+ * @param share the share to be sold
+ * @param week the week of the sale
+ * @return the sale transaction
+ */
+ @Override
+ protected Transaction createTransaction(Share share, int week) {
+ return new Sale(share, week);
+ }
+}
diff --git a/millions/src/main/java/no/ntnu/gruppe53/model/Share.java b/millions/src/main/java/no/ntnu/gruppe53/model/Share.java
new file mode 100644
index 0000000..229c473
--- /dev/null
+++ b/millions/src/main/java/no/ntnu/gruppe53/model/Share.java
@@ -0,0 +1,76 @@
+package no.ntnu.gruppe53.model;
+
+import java.math.BigDecimal;
+
+/**
+ * Represents a holding of a specific {@link Stock}.
+ *
+ *
A {@code Share} contains the quantity of shares owned and
+ * the price per share at the time of purchase. Future price
+ * fluctuations are not included in share.
+ *
+ */
+
+public class Share {
+ private final Stock stock;
+ private final BigDecimal quantity;
+ private final BigDecimal purchasePrice;
+
+ /**
+ * Creates a share for a given stock.
+ *
+ * @param stock the stock associated with the share; must not be {@code null}
+ * @param quantity the number of shares purchased; must not be {@code null}, negative, or zero
+ * @param purchasePrice price per share at the time of purchase;
+ * must not be {@code null}, negative or zero
+ * @throws IllegalArgumentException if {@code stock} is {@code null},
+ * or {@code quantity} is {@code null},
+ * negative, or zero, or {@code purchasePrice}
+ * is {@code null}, negative, or zero
+ */
+ public Share(Stock stock, BigDecimal quantity, BigDecimal purchasePrice) {
+ if (stock == null) {
+ throw new IllegalArgumentException("Stock cannot be null.");
+ }
+
+ if (quantity == null || quantity.signum() <= 0) {
+ throw new IllegalArgumentException("Quantity cannot be null, negative, or zero.");
+ }
+
+ if (purchasePrice == null || purchasePrice.signum() <= 0) {
+ throw new IllegalArgumentException("PurchasePrice cannot be null, negative, or zero.");
+ }
+
+ this.stock = stock;
+ this.quantity = quantity;
+ this.purchasePrice = purchasePrice;
+ }
+
+ /**
+ * Returns the stock the share is associated with.
+ *
+ * @return the stock
+ */
+ public Stock getStock() {
+ return stock;
+ }
+
+ /**
+ * Returns the quantity of shares owned in the stock.
+ *
+ * @return the quantity of shares
+ */
+
+ public BigDecimal getQuantity() {
+ return quantity;
+ }
+
+ /**
+ * Returns the purchase price per share for the stock at time of purchase.
+ *
+ * @return the purchase price of the shares.
+ */
+ public BigDecimal getPurchasePrice() {
+ return purchasePrice;
+ }
+}
diff --git a/millions/src/main/java/no/ntnu/gruppe53/model/Stock.java b/millions/src/main/java/no/ntnu/gruppe53/model/Stock.java
new file mode 100644
index 0000000..04c53c8
--- /dev/null
+++ b/millions/src/main/java/no/ntnu/gruppe53/model/Stock.java
@@ -0,0 +1,163 @@
+package no.ntnu.gruppe53.model;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+
+/**
+ * Represents a stock with its ticker symbol, company name,
+ * and a list of sales prices.
+ *
+ *
The list is a historical representation of price changes
+ * over time for the given stock.
+ *
+ *
+ *
The most recently added price represents the current market price.
+ *
+ *
+ *
A {@code Stock} instance always contains at least one price,
+ * instantiated at construction.
+ */
+public class Stock {
+ private final String symbol;
+ private final String company;
+ private final List prices;
+
+ private final ObjectProperty salesPrice;
+
+ /**
+ * Creates a stock with an already defined sales price.
+ *
+ * @param symbol the stock's unique ticker symbol (e.g. "AAPL"); must not be {@code null} or blank
+ * @param company the full name of the company; must not be {@code null} or blank
+ * @param salesPrice the current price per share; must not be {@code null}, negative, or zero
+ * @throws IllegalArgumentException if {@code symbol} is {@code null} or blank, {@code company} is
+ * {@code null} or blank, or {@code salesPrice} is {@code null},
+ * negative, or zero
+ */
+ public Stock(
+ String symbol, String company, BigDecimal salesPrice) {
+
+ if (symbol == null || symbol.isBlank()) {
+ throw new IllegalArgumentException("Symbol cannot be null or empty.");
+ }
+
+ if (company == null || company.isBlank()) {
+ throw new IllegalArgumentException("Company cannot be null or empty.");
+ }
+
+ if (salesPrice == null || salesPrice.signum() <= 0) {
+ throw new IllegalArgumentException("SalesPrice cannot be null, negative, or zero.");
+ }
+
+ this.symbol = symbol;
+ this.company = company;
+ this.prices = new ArrayList<>();
+ this.prices.add(salesPrice);
+
+ this.salesPrice = new SimpleObjectProperty<>(salesPrice);
+ }
+
+ /**
+ * Returns the stock's unique ticker symbol.
+ *
+ * @return the stock's ticker symbol
+ */
+ public String getSymbol() {
+ return symbol;
+ }
+
+ /**
+ * Returns the full name of the company associated with the stock.
+ *
+ * @return the full name of the stock's company
+ */
+ public String getCompany() {
+ return company;
+ }
+
+ /**
+ * Returns the current sale price per share of the stock.
+ *
+ *
The current price is defined as the most recent price
+ * in the price history.
+ *
+ * @return the current sale price of the stock
+ */
+ public BigDecimal getSalesPrice() {
+ return salesPrice.get();
+ }
+
+ /**
+ * Adds a new sales price to the stock's price history.
+ *
+ *
The added price becomes the new current market price.
+ *
+ *
The new price must be positive.
+ *
+ * @param price the new market price per share of the stock;
+ * must not be {@code null}, negative or zero
+ * @throws IllegalArgumentException if {@code price} is {@code null}, negative, or zero
+ */
+ public void addNewSalesPrice(BigDecimal price) {
+ if (price == null || price.signum() <= 0) {
+ throw new IllegalArgumentException("Price cannot be null, negative, or zero.");
+ }
+
+ this.prices.add(price);
+ this.salesPrice.set(price);
+ }
+
+ /**
+ * Returns an {@link ObjectProperty} of the sales price.
+ *
+ * @return an observable of the sales price
+ */
+ public ObjectProperty salesPriceProperty() {
+ return salesPrice;
+ }
+
+ /**
+ * Returns a list of all the sales prices for this stock since (and including) week 1.
+ *
+ * @return a list of all the stock's sales prices
+ */
+ public List getHistoricalPrices() {
+ return prices;
+ }
+
+ /**
+ * Returns the maximum historical sales price since (and including) week 1 for the stock.
+ *
+ * @return the stock's highest sales price
+ */
+ public BigDecimal getHighestPrice() {
+ return Collections.max(prices);
+ }
+
+ /**
+ * Returns the minimum historical sales price since (and including) week 1 for the stock.
+ *
+ * @return the stock's lowest sales price
+ */
+ public BigDecimal getLowestPrice() {
+ return Collections.min(prices);
+ }
+
+ /**
+ * Returns the sales price increase/decrease from previous week to this week. Returns 0 if
+ * the price list only has 1 value.
+ *
+ * @return the net difference in sales price between the last and second last week
+ */
+ public BigDecimal getLatestPriceChange() {
+ if (prices.size() >= 2) {
+ return prices.getLast().subtract(prices.get(prices.size() - 2));
+ } else {
+ return BigDecimal.ZERO;
+ }
+ }
+}
diff --git a/millions/src/main/java/no/ntnu/gruppe53/model/Transaction.java b/millions/src/main/java/no/ntnu/gruppe53/model/Transaction.java
new file mode 100644
index 0000000..ee2f11c
--- /dev/null
+++ b/millions/src/main/java/no/ntnu/gruppe53/model/Transaction.java
@@ -0,0 +1,94 @@
+package no.ntnu.gruppe53.model;
+
+/**
+ * Represents a financial transaction of a {@link Share}.
+ *
+ *
A transaction occurs in a specific week and uses a
+ * {@link TransactionCalculator} to determine the monetary change.
+ * Transactions are unique and is committed to a {@link Player}'s
+ * {@link Portfolio}.
+ *
+ *
Subclasses define the nature of the transaction (e.g. purchase
+ * or sale) by implementing the {@link #commit(Player)}
+ */
+public abstract class Transaction {
+ private final Share share;
+ private final int week;
+ private final TransactionCalculator calculator;
+ protected boolean committed;
+
+ /**
+ * Constructor for a transaction.
+ *
+ * @param share the share for the transaction; must not be {@code null}
+ * @param week the week the transaction takes place; must be greater than 0
+ * @param calculator the transaction calculator used to
+ * compute the {@link Transaction}
+ * @throws IllegalArgumentException if {@code share} is {@code null} or {@code week}
+ * is less than 1
+ */
+
+ protected Transaction(Share share, int week, TransactionCalculator calculator) {
+ if (week <= 0) {
+ throw new IllegalArgumentException("Week cannot be negative or zero");
+ }
+
+ this.share = share;
+ this.week = week;
+ this.calculator = calculator;
+ this.committed = false;
+ }
+
+ /**
+ * Returns the share for the current transaction.
+ *
+ * @return the share belonging to the transaction
+ */
+ public Share getShare() {
+ return this.share;
+ }
+
+ /**
+ * Returns the week the transaction took place.
+ *
+ * @return the week for the transaction
+ */
+ public int getWeek() {
+ return this.week;
+ }
+
+ /**
+ * Returns the transaction calculator used to calculate
+ * the transaction.
+ *
+ * @return the transaction calculator used for the transaction
+ */
+
+ public TransactionCalculator getCalculator() {
+ return this.calculator;
+ }
+
+ /**
+ * Returns whether the transaction has been committed to a player.
+ *
+ * @return {@code true} if committed, {@code false} otherwise
+ */
+
+ public boolean isCommitted() {
+ return this.committed;
+ }
+
+ /**
+ * Commits the transaction to a player, making it unique.
+ *
+ *
Subclasses should define the business logic required before
+ * committing. Then {@code super.commit(player)} should be called
+ * once the transaction has been successfully applied.
+ *
+ * @param player the player performing the transaction; must not be {@code null}
+ */
+
+ public void commit(Player player) {
+ this.committed = true;
+ }
+}
diff --git a/millions/src/main/java/no/ntnu/gruppe53/model/TransactionArchive.java b/millions/src/main/java/no/ntnu/gruppe53/model/TransactionArchive.java
new file mode 100644
index 0000000..88e5f7b
--- /dev/null
+++ b/millions/src/main/java/no/ntnu/gruppe53/model/TransactionArchive.java
@@ -0,0 +1,145 @@
+package no.ntnu.gruppe53.model;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+
+/**
+ * Maintains a history of all completed {@link Transaction}s.
+ *
+ *
A {@code TransactionArchive} stores transactions in the order
+ * they are added and provides methods to retrieve transactions
+ * filtered by type and week.
+ */
+
+public class TransactionArchive {
+ private final ObservableList transactions;
+
+ /**
+ * Creates an empty transaction archive.
+ */
+
+ public TransactionArchive() {
+ this.transactions = FXCollections.observableArrayList();
+ }
+
+ /**
+ * Adds a transaction to the archive.
+ *
+ * @param transaction the transaction being added; must not be {@code null}
+ * @return {@code true} if the transaction was successfully added
+ * {@code false} otherwise
+ * @throws IllegalArgumentException if {@code transaction} is {@code null}
+ */
+
+ public boolean add(Transaction transaction) {
+ if (transaction == null) {
+ throw new IllegalArgumentException("Transaction cannot be null.");
+ }
+
+ this.transactions.add(transaction);
+
+ return this.getTransactions().get(transactions.size() - 1).equals(transaction);
+ }
+
+ /**
+ * Checks whether there are no recorded transactions in the archive.
+ *
+ * @return {@code true} if the archive is empty.
+ * {@code false} otherwise
+ */
+
+ public boolean isEmpty() {
+ return this.transactions.isEmpty();
+ }
+
+ /**
+ * Returns all the transactions contained in the archive.
+ *
+ * @return the list of transactions
+ */
+
+ public ObservableList getObservableTransactions() {
+ return this.transactions;
+ }
+
+ /**
+ * Returns the transactions in the transaction archive.
+ *
+ * @return the transactions in the transaction archive
+ */
+ public List getTransactions() {
+ return this.transactions;
+ }
+
+ /**
+ * Returns all {@link Purchase} transactions for the given week.
+ *
+ * @param week the week number for transactions; must be greater than 0
+ * @return a list of purchase transactions for the given week;
+ * empty if none exist
+ * @throws IllegalArgumentException if {@code week} is not greater than 0
+ */
+
+ public List getPurchases(int week) {
+ if (week <= 0) {
+ throw new IllegalArgumentException("Week must be greater than 0.");
+ }
+ var purchaseList = new ArrayList();
+
+ for (Transaction t : this.transactions) {
+ if (t instanceof Purchase && t.getWeek() == week) {
+ purchaseList.add((Purchase) t);
+ }
+ }
+ return purchaseList;
+ }
+
+ /**
+ * Returns all {@link Sale} transactions for the given week.
+ *
+ * @param week the week number for transactions; must be greater than 0
+ * @return a list of sale transactions for the given week;
+ * empty if none exist
+ * @throws IllegalArgumentException if {@code week} is not greater than 0
+ */
+
+ public List getSales(int week) {
+ if (week <= 0) {
+ throw new IllegalArgumentException("Week must be greater than 0.");
+ }
+
+ var saleList = new ArrayList();
+
+ for (Transaction t : this.transactions) {
+ if (t instanceof Sale && t.getWeek() == week) {
+ saleList.add((Sale) t);
+ }
+ }
+ return saleList;
+ }
+
+ /**
+ * Counts the number of distinct week numbers represented in the
+ * transaction archive.
+ *
+ *
A week is considered distinct if at least one transaction has
+ * occurred within that week. Multiple transactions in the span of the
+ * same week will only be counted as one distinct week.
+ *
+ * @return the number of unique week values in the archive;
+ * {@code 0} if the archive contains no transactions
+ */
+
+ public int countDistinctWeeks() {
+ var distinctWeeks = new HashSet();
+
+ for (Transaction t : this.transactions) {
+ distinctWeeks.add(t.getWeek());
+ }
+
+ return distinctWeeks.size();
+ }
+}
diff --git a/millions/src/main/java/no/ntnu/gruppe53/model/TransactionCalculator.java b/millions/src/main/java/no/ntnu/gruppe53/model/TransactionCalculator.java
new file mode 100644
index 0000000..c9f2097
--- /dev/null
+++ b/millions/src/main/java/no/ntnu/gruppe53/model/TransactionCalculator.java
@@ -0,0 +1,39 @@
+package no.ntnu.gruppe53.model;
+
+import java.math.BigDecimal;
+
+/**
+ * Represents the necessary calculations to perform a transaction.
+ *
+ *
{@link Purchase} and {@link Sale} are examples of implementations of a transaction.
+ */
+public interface TransactionCalculator {
+ /**
+ * Calculates the gross value of a transaction.
+ *
+ * @return the gross value of a transaction
+ */
+ BigDecimal calculateGross();
+
+ /**
+ * Calculates the commission for the exchange of a transaction.
+ *
+ * @return the commission of a transaction
+ */
+ BigDecimal calculateCommission();
+
+ /**
+ * Calculates the tax of a transaction.
+ *
+ * @return the tax of a transaction
+ */
+ BigDecimal calculateTax();
+
+ /**
+ * Returns the total value of a transaction.
+ *
+ * @return the total value of a transaction
+ */
+ BigDecimal calculateTotal();
+
+}
diff --git a/millions/src/main/java/no/ntnu/gruppe53/model/TransactionFactory.java b/millions/src/main/java/no/ntnu/gruppe53/model/TransactionFactory.java
new file mode 100644
index 0000000..cb27b9f
--- /dev/null
+++ b/millions/src/main/java/no/ntnu/gruppe53/model/TransactionFactory.java
@@ -0,0 +1,40 @@
+package no.ntnu.gruppe53.model;
+
+/**
+ * Factory for creating a {@link Transaction} for a {@link Share} in an {@link Exchange}.
+ *
+ *
Subclasses implement the specific type of transaction to be used.
+ */
+public abstract class TransactionFactory {
+
+ /**
+ * Creates a transaction.
+ *
+ * @param share the share involved
+ * @param week the transaction week
+ * @return a transaction
+ */
+ public Transaction create(Share share, int week) {
+ if (share == null) {
+ throw new IllegalArgumentException("Share cannot be null.");
+ }
+
+ if (week <= 0) {
+ throw new IllegalArgumentException("Week must be positive whole number.");
+ }
+
+ return createTransaction(share, week);
+ }
+
+ /**
+ * Creates a transaction from a share and week.
+ *
+ * @param share the share for the transaction
+ * @param week the week of the transaction
+ * @return a transaction for the share and week
+ */
+ protected abstract Transaction createTransaction(
+ Share share, int week
+ );
+
+}
diff --git a/millions/src/main/java/no/ntnu/gruppe53/service/FileHandler.java b/millions/src/main/java/no/ntnu/gruppe53/service/FileHandler.java
new file mode 100644
index 0000000..a587c09
--- /dev/null
+++ b/millions/src/main/java/no/ntnu/gruppe53/service/FileHandler.java
@@ -0,0 +1,155 @@
+package no.ntnu.gruppe53.service;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+import no.ntnu.gruppe53.model.Stock;
+
+/**
+ * Handles reading and writing of .csv files for stocks.
+ */
+public class FileHandler {
+ /**
+ * Writes a list of {@link Stock}s to a CSV file in UTF-8 encoding.
+ *
+ * @param stockList the list of stocks to be written
+ * @param path directory path for the file
+ * @param filename name of the file (without or with .csv extension)
+ * @throws IllegalArgumentException if stockList, path,
+ * or filename is null, or if stockList is empty
+ */
+ public static void writeStocksToFile(List stockList, String path, String filename) {
+ if (stockList == null) {
+ throw new IllegalArgumentException("StockList cannot be null");
+ }
+
+ if (path == null || path.isBlank()) {
+ throw new IllegalArgumentException("Path cannot be null or blank");
+ }
+
+ if (filename == null || filename.isBlank()) {
+ throw new IllegalArgumentException(
+ "Filename cannot be null or blank");
+ }
+
+ path = path.trim();
+ filename = filename.trim();
+ String filenameWithExtension = filename.endsWith(".csv") ? filename : filename + ".csv";
+
+ Path filePath = Paths.get(path, filenameWithExtension);
+
+ Path dirPath = filePath.getParent();
+
+ if (!Files.exists(dirPath)) {
+ try {
+ Files.createDirectories(dirPath);
+ } catch (IOException e) {
+ System.out.println("ERROR: Could not create directories.");
+ e.printStackTrace();
+ return;
+ }
+ }
+
+ try (BufferedWriter writer = Files.newBufferedWriter(filePath, StandardCharsets.UTF_8)) {
+ writer.write("#Ticker,Name,Price");
+ writer.newLine();
+
+ for (Stock stock : stockList) {
+ if (stock == null) {
+ continue;
+ }
+
+ String line = String.join(",",
+ stock.getSymbol(),
+ stock.getCompany(),
+ stock.getSalesPrice().toString());
+ writer.write(line);
+ writer.newLine();
+ }
+
+ } catch (IOException e) {
+ System.out.println("ERROR: Something went wrong while writing the CSV file.");
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Reads a CSV file and converts it to a list of {@link Stock} objects.
+ *
+ * @param path directory path of the CSV file
+ * @param filename CSV file name (with or without .csv extension)
+ * @return list of Stock objects
+ * @throws IllegalArgumentException if path or filename is null or blank
+ */
+ public static List readStocksFromFile(String path, String filename) {
+ if (path == null || path.isBlank()) {
+ throw new IllegalArgumentException("Path cannot be null or blank.");
+ }
+
+ if (filename == null || filename.isBlank()) {
+ throw new IllegalArgumentException("Filename cannot be null or blank.");
+ }
+
+ path = path.trim();
+ filename = filename.trim();
+ String filenameWithExtension = filename.endsWith(".csv") ? filename : filename + ".csv";
+
+ Path filePath = Paths.get(path, filenameWithExtension);
+
+ List stocks = new ArrayList<>();
+ if (!Files.exists(filePath)) {
+ System.out.println("ERROR: File not found: " + filePath.toAbsolutePath());
+ return stocks;
+ }
+
+ try (BufferedReader reader = Files.newBufferedReader(filePath, StandardCharsets.UTF_8)) {
+ String line;
+
+ while ((line = reader.readLine()) != null) {
+ line = line.trim();
+
+ if (line.isBlank() || line.startsWith("#")) {
+ continue;
+ }
+
+ String[] values = line.split(",", -1);
+
+ if (values.length != 3) {
+ System.out.println("ERROR: Bad line in CSV (expected 3 values): " + line);
+ continue;
+ }
+
+ String symbol = values[0].trim();
+ String name = values[1].trim();
+ BigDecimal price;
+
+ try {
+ price = new BigDecimal(values[2].trim());
+ } catch (NumberFormatException e) {
+ System.out.println("ERROR: Price is not a valid number: " + values[2]);
+ continue;
+ }
+
+ if (symbol.isBlank() || name.isBlank() || price.compareTo(BigDecimal.ZERO) <= 0) {
+ System.out.println("ERROR: Invalid stock data, skipping: " + line);
+ continue;
+ }
+
+ stocks.add(new Stock(symbol, name, price));
+ }
+
+ } catch (IOException e) {
+ System.out.println("ERROR: Something went wrong while reading the CSV file.");
+ e.printStackTrace();
+ }
+
+ return stocks;
+ }
+}
\ No newline at end of file
diff --git a/millions/src/main/java/no/ntnu/gruppe53/service/FormatBigDecimal.java b/millions/src/main/java/no/ntnu/gruppe53/service/FormatBigDecimal.java
new file mode 100644
index 0000000..422b321
--- /dev/null
+++ b/millions/src/main/java/no/ntnu/gruppe53/service/FormatBigDecimal.java
@@ -0,0 +1,21 @@
+package no.ntnu.gruppe53.service;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+
+/**
+ * Service/helper class that formats a {@link BigDecimal} to a string with 2 decimal accuracy.
+ */
+public class FormatBigDecimal {
+ /**
+ * Static method used to format a Big Decimal to a string representation with 2 decimals accuracy.
+ *
+ *
Returns "0.00" if the BigDecimal is null.
+ *
+ * @param number the BigDecimal to be formated
+ * @return the string representation of the BigDecimal with 2 decimals accuracy.
+ */
+ public static String formatNumber(BigDecimal number) {
+ return number == null ? "0.00" : number.setScale(2, RoundingMode.HALF_UP).toPlainString();
+ }
+}
diff --git a/millions/src/main/java/no/ntnu/gruppe53/service/LanguageManager.java b/millions/src/main/java/no/ntnu/gruppe53/service/LanguageManager.java
new file mode 100644
index 0000000..6d6356d
--- /dev/null
+++ b/millions/src/main/java/no/ntnu/gruppe53/service/LanguageManager.java
@@ -0,0 +1,101 @@
+package no.ntnu.gruppe53.service;
+
+import java.util.Locale;
+import java.util.ResourceBundle;
+import javafx.beans.binding.Bindings;
+import javafx.beans.binding.StringBinding;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+
+/**
+ * Manages the language used in the user-interface.
+ *
+ *
The possible languages to be used are set by {@link LanguageRegistry}.
+ */
+public class LanguageManager {
+
+ /**
+ * Sets a default language and creates an observable {@link ResourceBundle}.
+ */
+ private final ObjectProperty bundleProperty =
+ new SimpleObjectProperty<>();
+
+ /**
+ * Sets the language to the default locale at first instantiation.
+ */
+ private LanguageManager() {
+ setLocale(LanguageRegistry.getDefaultLocale());
+ }
+
+ /**
+ * Static Inner Class to lazily create the instance.
+ */
+ private static class LanguageManagerInner {
+ private static final LanguageManager INSTANCE =
+ new LanguageManager();
+ }
+
+ /**
+ * Returns the instance of the language manager.
+ *
+ * @return the instance of the language manager
+ */
+ public static LanguageManager getInstance() {
+ return LanguageManagerInner.INSTANCE;
+ }
+
+ /**
+ * Returns the observable resource bundle.
+ *
+ * @return an observable resource bundle
+ */
+ public ObjectProperty getBundleProperty() {
+ return bundleProperty;
+ }
+
+ /**
+ * Sets the locale of the instance to a given locale which determines the properties file to load
+ * values from.
+ *
+ * @param locale the locale to change to
+ */
+ public void setLocale(Locale locale) {
+ ResourceBundle bundle =
+ ResourceBundle.getBundle("i18n.lang", locale);
+
+ bundleProperty.set(bundle);
+ }
+
+ /**
+ * Returns the string value of the given key in a .properties file.
+ *
+ * @param key the key to look for in the .properties file
+ * @return the value of the key
+ */
+ public String getString(String key) {
+ return bundleProperty.get().getString(key);
+ }
+
+ /**
+ * Returns the current locale.
+ *
+ * @return the current locale
+ */
+ public Locale getLocale() {
+ return bundleProperty.get().getLocale();
+ }
+
+ /**
+ * Creates a {@link StringBinding} to a given string and associates it
+ * with a given key in a .properties file.
+ *
+ * @param key the key to associate the string binding with
+ * @return a string binding observable
+ */
+ public StringBinding bindString(String key) {
+ return Bindings.createStringBinding(
+ () -> getString(key),
+ getBundleProperty()
+ );
+ }
+}
\ No newline at end of file
diff --git a/millions/src/main/java/no/ntnu/gruppe53/service/LanguageOption.java b/millions/src/main/java/no/ntnu/gruppe53/service/LanguageOption.java
new file mode 100644
index 0000000..cd607b1
--- /dev/null
+++ b/millions/src/main/java/no/ntnu/gruppe53/service/LanguageOption.java
@@ -0,0 +1,12 @@
+package no.ntnu.gruppe53.service;
+
+import java.util.Locale;
+
+/**
+ * Acts as a data-carrier representing a language with a string name and a {@link Locale}.
+ *
+ * @param name the name of the language
+ * @param locale the locale of the language
+ */
+public record LanguageOption(String name, Locale locale) {}
+
diff --git a/millions/src/main/java/no/ntnu/gruppe53/service/LanguageRegistry.java b/millions/src/main/java/no/ntnu/gruppe53/service/LanguageRegistry.java
new file mode 100644
index 0000000..490ab9f
--- /dev/null
+++ b/millions/src/main/java/no/ntnu/gruppe53/service/LanguageRegistry.java
@@ -0,0 +1,56 @@
+package no.ntnu.gruppe53.service;
+
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Represents a registry of allowed {@link LanguageOption}s.
+ */
+public class LanguageRegistry {
+
+ /**
+ * Sets all possible languages that can be used.
+ */
+ private static final List languages = List.of(
+ new LanguageOption("English", new Locale("en")),
+ new LanguageOption("Norsk", new Locale("no"))
+ );
+
+ /**
+ * Sets the default language to be used.
+ *
+ *
Searches through the registry to find english and sets it as default.
+ */
+ private static final LanguageOption defaultLang =
+ languages.stream()
+ .filter(lang -> lang.locale().getLanguage().equals("en"))
+ .findFirst()
+ .orElse(languages.getFirst());
+
+ /**
+ * Returns all registered languages.
+ *
+ * @return a list of registered languages
+ */
+ public static List getLanguages() {
+ return languages;
+ }
+
+ /**
+ * Returns the default language.
+ *
+ * @return the default language
+ */
+ public static LanguageOption getDefaultLanguage() {
+ return defaultLang;
+ }
+
+ /**
+ * Returns the default locale of the default language.
+ *
+ * @return the default locale
+ */
+ public static Locale getDefaultLocale() {
+ return defaultLang.locale();
+ }
+}
diff --git a/millions/src/main/java/no/ntnu/gruppe53/service/StockLineChartService.java b/millions/src/main/java/no/ntnu/gruppe53/service/StockLineChartService.java
new file mode 100644
index 0000000..5e785cd
--- /dev/null
+++ b/millions/src/main/java/no/ntnu/gruppe53/service/StockLineChartService.java
@@ -0,0 +1,79 @@
+package no.ntnu.gruppe53.service;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.List;
+import javafx.scene.chart.XYChart;
+import no.ntnu.gruppe53.model.Exchange;
+import no.ntnu.gruppe53.model.Stock;
+import no.ntnu.gruppe53.view.StockLineChartView;
+
+/**
+ * Builds the {@link XYChart.Series} values for the {@link StockLineChartView}.
+ */
+
+public class StockLineChartService {
+
+ /**
+ * Builds the {@code XYChart.Series} for the {@code LineChart} of a single {@link Stock}.
+ *
+ *
Returns the {@code XYChart.Series}.
+ *
+ * @param stock the stock whose price values will be shown in the LineChart
+ * @param exchange the exchange containing the stock whose
+ * week number will be shown in the LineChart
+ * @return the {@code XYChart.Series} with
+ * the y-values from the price history of the stock,
+ * and the x-values from the week number of the exchange
+ * @throws IllegalArgumentException if there are more x-values than y-values or vice versa
+ */
+
+ public static XYChart.Series buildSeries(Stock stock, Exchange exchange) {
+ XYChart.Series series = new XYChart.Series<>();
+ series.setName(stock.getSymbol());
+
+ // Check for 1-1 ratio of price and week list size for plotting
+ if (stock.getHistoricalPrices().size() != exchange.getWeek()) {
+ throw new IllegalArgumentException("Invalid Data: mismatch between prices and weeks. "
+ + "Should have 1-1 ratio.");
+ }
+
+ int week = 1;
+
+ for (BigDecimal price : stock.getHistoricalPrices()) {
+ series.getData().add(
+ new XYChart.Data<>(week, price.doubleValue()));
+ week++;
+ }
+
+ return series;
+ }
+
+ /**
+ * Builds the {@code XYChart.Series} for the {@code LineChart} of a list of {@code Stock}s.
+ *
+ *
Returns a list of the {@code XYChart.Series} for each stock.
+ *
+ * @param stocks the list of stocks whose price values will be shown
+ * in the LineChart
+ * @param exchange the exchange containing the stock whose week number
+ * will be shown in the LineChart
+ * @return the list of {@code XYChart.Series} with
+ * the y-values from the price history of the stock,
+ * and the x-values from the week number of the exchange
+ */
+
+ public static List> buildSeries(
+ List stocks, Exchange exchange) {
+ List> seriesList = new ArrayList<>();
+
+ for (Stock stock : stocks) {
+ XYChart.Series series =
+ buildSeries(stock, exchange);
+
+ seriesList.add(series);
+ }
+
+ return seriesList;
+ }
+}
\ No newline at end of file
diff --git a/millions/src/main/java/no/ntnu/gruppe53/view/CsvChooserView.java b/millions/src/main/java/no/ntnu/gruppe53/view/CsvChooserView.java
new file mode 100644
index 0000000..4264887
--- /dev/null
+++ b/millions/src/main/java/no/ntnu/gruppe53/view/CsvChooserView.java
@@ -0,0 +1,51 @@
+package no.ntnu.gruppe53.view;
+
+import java.io.File;
+import javafx.scene.control.Alert;
+import javafx.stage.FileChooser;
+import javafx.stage.Stage;
+import no.ntnu.gruppe53.service.LanguageManager;
+
+/**
+ * Represents an {@link Alert} and {@link FileChooser} for
+ * informing user of .csv requirement and picking said file.
+ *
+ *
Uses a {@link LanguageManager} to insert set text to target language.
+ */
+public class CsvChooserView {
+ private final FileChooser fileChooser = new FileChooser();
+ Alert newGameAlert = new Alert(Alert.AlertType.INFORMATION);
+ private final LanguageManager lm = LanguageManager.getInstance();
+
+ /**
+ * Returns the dialog for the alert and file chooser and exposes it to controllers.
+ *
+ * @param stage the stage for the dialog to be shown
+ * @return the dialog for the alert and file chooser
+ */
+ public File getDialog(Stage stage) {
+ // Notifies the user about .csv selection
+ newGameAlert.setTitle(null);
+ newGameAlert.headerTextProperty().bind(lm.bindString("csvHeader"));
+ newGameAlert.contentTextProperty().bind(lm.bindString("csvContent"));
+
+ // Sets the dimensions of the alert to show all text, and as a band-aid, be resizable
+ newGameAlert.setResizable(true);
+
+ newGameAlert.getDialogPane().setPrefWidth(450);
+ newGameAlert.getDialogPane().setPrefHeight(350);
+
+ newGameAlert.showAndWait();
+
+ // File chooser to choose .csv file
+ fileChooser.setTitle(lm.getString("csvFileChooserTitle"));
+
+ // Limits to .csv file only
+ fileChooser.getExtensionFilters().clear();
+ fileChooser.getExtensionFilters().add(
+ new FileChooser.ExtensionFilter(lm.getString("csvFileFilterLabel"), "*.csv")
+ );
+
+ return fileChooser.showOpenDialog(stage);
+ }
+}
\ No newline at end of file
diff --git a/millions/src/main/java/no/ntnu/gruppe53/view/EndView.java b/millions/src/main/java/no/ntnu/gruppe53/view/EndView.java
new file mode 100644
index 0000000..611e997
--- /dev/null
+++ b/millions/src/main/java/no/ntnu/gruppe53/view/EndView.java
@@ -0,0 +1,213 @@
+package no.ntnu.gruppe53.view;
+
+import javafx.geometry.Insets;
+import javafx.geometry.Pos;
+import javafx.scene.control.Button;
+import javafx.scene.control.Label;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.Region;
+import javafx.scene.layout.VBox;
+import javafx.scene.text.TextAlignment;
+import no.ntnu.gruppe53.service.LanguageManager;
+
+/**
+ * Represents the End Game screen, showing the Level the Player reached,
+ * money earned in total after selling portfolio and gives the option
+ * to quit the program or start a new game.
+ */
+public class EndView extends BorderPane {
+ private Button newGameButton;
+ private Button quitButton;
+
+ private Label statusLabel;
+ private Label moneyLabel;
+
+ private final LanguageManager lm = LanguageManager.getInstance();
+
+ /**
+ * Sets the centerPane to the constructed centerPane.
+ */
+ public EndView() {
+ this.setCenter(centerPane());
+ }
+
+ /**
+ * Constructs the centerPane using a {@link BorderPane}.
+ *
+ * @return a constructed borderpane
+ */
+ private BorderPane centerPane() {
+ BorderPane pane = new BorderPane();
+ pane.setPadding(new Insets(15, 12, 15, 12));
+ // pane.setSpacing(10);
+ // Blue Background color
+ pane.setStyle("-fx-background-color: #00bcf0;");
+
+ Label congratsLabel = new Label();
+ congratsLabel.textProperty().bind(lm.bindString("congratsLabel"));
+ congratsLabel.setStyle("-fx-font-size: 18");
+ congratsLabel.setTextAlignment(TextAlignment.CENTER);
+
+ HBox congratsBox = new HBox();
+ congratsBox.setStyle("-fx-background-color: #ffe556;"
+ + "-fx-text-fill: #303539;"
+ + "-fx-background-radius: 8;"
+ + "-fx-border-color: #303539;"
+ + "-fx-border-radius: 8;"
+ );
+
+ congratsBox.setPadding(new Insets(8, 14, 8, 14));
+ congratsBox.setAlignment(Pos.CENTER);
+ congratsBox.setMinHeight(65);
+
+ congratsBox.getChildren().add(congratsLabel);
+
+ Label statusLabelText = new Label();
+ statusLabelText.textProperty().bind(lm.bindString("statusLabelText"));
+ statusLabelText.setStyle("-fx-font-size: 18");
+ statusLabel = new Label();
+ statusLabel.setStyle("-fx-font-size: 18");
+
+ HBox statusLabelBox = new HBox();
+ statusLabelBox.setStyle("-fx-background-color: #ffe556;"
+ + "-fx-text-fill: #303539;"
+ + "-fx-background-radius: 8;"
+ + "-fx-border-color: #303539;"
+ + "-fx-border-radius: 8;"
+ );
+
+ statusLabelBox.setPadding(new Insets(8, 14, 8, 14));
+ statusLabelBox.setAlignment(Pos.CENTER);
+ statusLabelBox.setMinHeight(65);
+
+ statusLabelBox.getChildren().addAll(statusLabelText, statusLabel);
+
+ Label moneyLabelText = new Label();
+ moneyLabelText.textProperty().bind(lm.bindString("navMoneyLabelText"));
+ moneyLabelText.setStyle("-fx-font-size: 18");
+ moneyLabel = new Label();
+ moneyLabel.setStyle("-fx-font-size: 18");
+
+ HBox moneyLabelBox = new HBox();
+ moneyLabelBox.setStyle("-fx-background-color: #ffe556;"
+ + "-fx-text-fill: #303539;"
+ + "-fx-background-radius: 8;"
+ + "-fx-border-color: #303539;"
+ + "-fx-border-radius: 8;"
+ );
+
+ moneyLabelBox.setPadding(new Insets(8, 14, 8, 14));
+ moneyLabelBox.setAlignment(Pos.CENTER);
+ moneyLabelBox.setMinHeight(65);
+
+ moneyLabelBox.getChildren().addAll(moneyLabelText, moneyLabel);
+
+ VBox centerPane = new VBox();
+ centerPane.setPadding(new Insets(15, 12, 15, 12));
+ centerPane.setAlignment(Pos.CENTER);
+ centerPane.setSpacing(10);
+ centerPane.setMaxWidth(700);
+
+
+ centerPane.getChildren().addAll(congratsBox, statusLabelBox, moneyLabelBox);
+
+ quitButton = new Button();
+ quitButton.textProperty().bind(lm.bindString("quitGame"));
+ quitButton.setPrefSize(200, 50);
+ quitButton.setStyle("-fx-text-fill: #303539;"
+ + "-fx-font-size: 18px;"
+ + "-fx-border-color: #303539;"
+ + "-fx-border-radius: 8;"
+ + "-fx-background-radius: 8;"
+ );
+
+ HBox quitButtonBox = new HBox();
+ quitButtonBox.setStyle("-fx-background-color: #ffe556;"
+ + "-fx-text-fill: #303539;"
+ + "-fx-background-radius: 8;"
+ + "-fx-border-color: #303539;"
+ + "-fx-border-radius: 8;"
+ );
+
+ quitButtonBox.setPadding(new Insets(8, 14, 8, 14));
+ quitButtonBox.setAlignment(Pos.CENTER);
+
+
+ quitButtonBox.getChildren().addAll(quitButton);
+
+ newGameButton = new Button();
+ newGameButton.textProperty().bind(lm.bindString("newGame"));
+ newGameButton.setPrefSize(200, 50);
+ newGameButton.setStyle("-fx-text-fill: #303539;"
+ + "-fx-font-size: 18px;"
+ + "-fx-border-color: #303539;"
+ + "-fx-border-radius: 8;"
+ + "-fx-background-radius: 8;"
+ );
+
+ HBox newGameButtonBox = new HBox();
+ newGameButtonBox.setStyle("-fx-background-color: #ffe556;"
+ + "-fx-text-fill: #303539;"
+ + "-fx-background-radius: 8;"
+ + "-fx-border-color: #303539;"
+ + "-fx-border-radius: 8;"
+ );
+
+ newGameButtonBox.setPadding(new Insets(8, 14, 8, 14));
+ newGameButtonBox.setAlignment(Pos.CENTER);
+ newGameButtonBox.getChildren().add(newGameButton);
+
+ HBox buttons = new HBox(quitButtonBox, newGameButtonBox);
+
+ Region spacerBottomLeft = new Region();
+ Region spacerBottomRight = new Region();
+
+ HBox.setHgrow(spacerBottomLeft, Priority.ALWAYS);
+ HBox.setHgrow(spacerBottomRight, Priority.ALWAYS);
+
+ HBox bottomPane = new HBox(spacerBottomLeft, buttons, spacerBottomRight);
+
+ pane.setCenter(centerPane);
+ pane.setBottom(bottomPane);
+
+ return pane;
+ }
+
+ /**
+ * Sets the action to be run when clicking the quit button.
+ *
+ * @param action the action to run when clicking the quit button
+ */
+ public void onQuitButton(Runnable action) {
+ quitButton.setOnAction(e -> action.run());
+ }
+
+ /**
+ * Sets the action to be run when clicking the New Game button.
+ *
+ * @param action the action to run when clicking the New Game button
+ */
+ public void onNewGameButton(Runnable action) {
+ newGameButton.setOnAction(e -> action.run());
+ }
+
+ /**
+ * Returns the statusLabel.
+ *
+ * @return the statusLabel
+ */
+ public Label getStatusLabel() {
+ return statusLabel;
+ }
+
+ /**
+ * Returns the moneyLabel.
+ *
+ * @return the moneyLabel
+ */
+ public Label getMoneyLabel() {
+ return moneyLabel;
+ }
+}
diff --git a/millions/src/main/java/no/ntnu/gruppe53/view/FooterBar.java b/millions/src/main/java/no/ntnu/gruppe53/view/FooterBar.java
new file mode 100644
index 0000000..dd9857b
--- /dev/null
+++ b/millions/src/main/java/no/ntnu/gruppe53/view/FooterBar.java
@@ -0,0 +1,233 @@
+package no.ntnu.gruppe53.view;
+
+import java.util.Locale;
+import java.util.function.Consumer;
+import javafx.geometry.Insets;
+import javafx.geometry.Pos;
+import javafx.scene.control.Button;
+import javafx.scene.control.ComboBox;
+import javafx.scene.control.Label;
+import javafx.scene.control.ListCell;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.Region;
+import javafx.util.Subscription;
+import no.ntnu.gruppe53.controller.GameController;
+import no.ntnu.gruppe53.model.Exchange;
+import no.ntnu.gruppe53.model.Stock;
+import no.ntnu.gruppe53.service.LanguageManager;
+import no.ntnu.gruppe53.service.LanguageOption;
+import no.ntnu.gruppe53.service.LanguageRegistry;
+
+/*
+Yellow Color, Hex: #ffe556 RGB(255, 229, 86)
+Blue Color, Hex: #00bcf0 RGB(0, 188, 240)
+Black Color, Hex: #303539 RGB(48, 53, 57)
+ */
+
+/**
+ * A {@link HBox} view representing the footer/bottom to be used in a {@link BorderPane} layout.
+ *
+ *
Uses a {@link LanguageManager} to change text to target language dynamically.
+ */
+public class FooterBar extends HBox {
+ private final Button advanceWeekButton;
+ private final Label currentWeekLabel;
+ private final Label biggestLoserLabel;
+ private final Label biggestLoserPriceLabel;
+ private final Label biggestWinnerLabel;
+ private final Label biggestWinnerPriceLabel;
+ private Subscription biggestLoserSubscription;
+ private Subscription biggestWinnerSubscription;
+
+ private final LanguageManager lm = LanguageManager.getInstance();
+
+ private final ComboBox languageBox;
+
+ /**
+ * Constructs the layout of the view and its components.
+ *
+ *
Constructs a {@link ComboBox} to let the user switch language on the fly.
+ *
+ *
Sets the text of the UI elements dynamically.
+ */
+ public FooterBar() {
+ this.setSpacing(10);
+ this.setPadding(new Insets(15, 12, 15, 12));
+ this.setStyle("-fx-background-color: #00bcf0;"
+ + "-fx-border-color: #303539 transparent transparent transparent;"
+ + "-fx-border-width: 5 0 0 0;");
+
+ languageBox = new ComboBox<>();
+ languageBox.getItems().addAll(LanguageRegistry.getLanguages());
+
+ Locale activeLocale = lm.getLocale();
+ LanguageOption activeOption = LanguageRegistry.getLanguages().stream()
+ .filter(lang -> lang.locale().equals(activeLocale))
+ .findFirst()
+ .orElse(LanguageRegistry.getDefaultLanguage());
+
+ languageBox.setValue(activeOption);
+
+ // Listens for changes in language from other sources (e.g. startView)
+ lm.getBundleProperty().subscribe(bundle -> {
+ java.util.Locale currentLocale = lm.getLocale();
+
+ LanguageOption matchingOption = LanguageRegistry.getLanguages().stream()
+ .filter(lang -> lang.locale().equals(currentLocale))
+ .findFirst()
+ .orElse(null);
+
+ if (matchingOption != null && !matchingOption.equals(languageBox.getValue())) {
+ languageBox.setValue(matchingOption);
+ }
+ });
+
+ languageBox.setCellFactory(list -> new ListCell<>() {
+ private Subscription cellSub;
+ @Override
+ protected void updateItem(LanguageOption item, boolean empty) {
+ super.updateItem(item, empty);
+ if (cellSub != null) {
+ cellSub.unsubscribe();
+ }
+
+ if (empty || item == null) {
+ setText(null);
+ } else {
+ cellSub = lm.getBundleProperty().subscribe(bundle -> setText(item.name()));
+ }
+ }
+ });
+
+ languageBox.setButtonCell(languageBox.getCellFactory().call(null));
+
+ Label biggestLoserLabelText = new Label();
+ biggestLoserLabelText.textProperty().bind(lm.bindString("biggestLoserLabelText"));
+ biggestLoserLabel = new Label();
+ biggestLoserPriceLabel = new Label();
+ biggestLoserPriceLabel.setStyle("-fx-text-fill: red;");
+
+ HBox loserBox = new HBox();
+ loserBox.setStyle("-fx-background-color: #ffe556;"
+ + "-fx-text-fill: #303539;"
+ + "-fx-background-radius: 8;"
+ + "-fx-border-color: #303539;"
+ + "-fx-border-radius: 8;"
+ );
+
+ loserBox.setPadding(new Insets(8, 14, 8, 14));
+ loserBox.setAlignment(Pos.CENTER);
+
+ loserBox.getChildren().addAll(biggestLoserLabelText, biggestLoserLabel, biggestLoserPriceLabel);
+
+ Label biggestWinnerLabelText = new Label();
+ biggestWinnerLabelText.textProperty().bind(lm.bindString("biggestWinnerLabelText"));
+ biggestWinnerLabel = new Label();
+ biggestWinnerPriceLabel = new Label();
+ biggestWinnerPriceLabel.setStyle("-fx-text-fill: green;");
+
+ HBox winnerBox = new HBox();
+ winnerBox.setStyle("-fx-background-color: #ffe556;"
+ + "-fx-text-fill: #303539;"
+ + "-fx-background-radius: 8;"
+ + "-fx-border-color: #303539;"
+ + "-fx-border-radius: 8;"
+ );
+
+ winnerBox.setPadding(new Insets(8, 14, 8, 14));
+ winnerBox.setAlignment(Pos.CENTER);
+
+ winnerBox.getChildren().addAll(
+ biggestWinnerLabelText, biggestWinnerLabel, biggestWinnerPriceLabel);
+
+
+ Region spacer = new Region();
+ HBox.setHgrow(spacer, Priority.ALWAYS);
+
+ HBox currentWeekBox = new HBox();
+ currentWeekBox.setSpacing(10);
+
+ currentWeekBox.setStyle("-fx-background-color: #ffe556;"
+ + "-fx-text-fill: #303539;"
+ + "-fx-background-radius: 8;"
+ + "-fx-border-color: #303539;"
+ + "-fx-border-radius: 8;"
+ );
+
+ currentWeekBox.setPadding(new Insets(8, 14, 8, 14));
+ currentWeekBox.setAlignment(Pos.CENTER);
+
+ Label currentWeekLabelText = new Label();
+ currentWeekLabelText.textProperty().bind(lm.bindString("currentWeekLabelText"));
+ currentWeekLabel = new Label();
+ currentWeekBox.getChildren().addAll(currentWeekLabelText, currentWeekLabel);
+
+ advanceWeekButton = new Button();
+ advanceWeekButton.textProperty().bind(lm.bindString("advanceWeekButton"));
+ advanceWeekButton.setStyle("-fx-background-color: #ffe556; -fx-text-fill: #303539;");
+
+ this.getChildren().addAll(
+ languageBox, loserBox, winnerBox, spacer, currentWeekBox, advanceWeekButton);
+ }
+
+ /**
+ * Sets the exchange and subscribes to week updates.
+ *
+ * @param exchange the {@link Exchange} used in the game
+ */
+ public void setExchange(Exchange exchange) {
+ Subscription currentWeekSubscription = exchange.getCurrentWeekProperty().subscribe(newVal -> {
+ if (exchange != null) {
+ currentWeekLabel.setText(newVal.toString());
+ } else {
+ currentWeekLabel.setText("None");
+ }
+ });
+
+ if (biggestLoserSubscription != null) {
+ biggestLoserSubscription.unsubscribe();
+ }
+
+ biggestLoserSubscription = exchange.getCurrentWeekProperty().subscribe(newWeek -> {
+ Stock loser = exchange.getLosers(1).getFirst();
+ biggestLoserLabel.setText(loser.getSymbol() + " - " + loser.getCompany());
+ biggestLoserPriceLabel.setText(" $" + loser.getLatestPriceChange());
+ });
+
+ if (biggestWinnerSubscription != null) {
+ biggestWinnerSubscription.unsubscribe();
+ }
+
+ biggestWinnerSubscription = exchange.getCurrentWeekProperty().subscribe(newWeek -> {
+ Stock winner = exchange.getGainers(1).getFirst();
+ biggestWinnerLabel.setText(winner.getSymbol() + " - " + winner.getCompany());
+ biggestWinnerPriceLabel.setText(" $" + winner.getLatestPriceChange());
+ });
+ }
+
+ /**
+ * Exposes the advanceWeekButton to {@link GameController} so it can
+ * advance the week of the {@link Exchange}.
+ *
+ * @param action the action to be performed when clicking the button
+ */
+ public void onAdvanceButtonClick(Runnable action) {
+ advanceWeekButton.setOnAction(e -> action.run());
+ }
+
+ /**
+ * Exposes the LanguageOption ComboButton to the controller.
+ *
+ * @param action the action to be performed when selecting the language
+ */
+ public void onLanguageChange(Consumer action) {
+ languageBox.setOnAction(e -> {
+ LanguageOption selected = languageBox.getValue();
+ if (selected != null) {
+ action.accept(selected);
+ }
+ });
+ }
+}
\ No newline at end of file
diff --git a/millions/src/main/java/no/ntnu/gruppe53/view/MarketView.java b/millions/src/main/java/no/ntnu/gruppe53/view/MarketView.java
new file mode 100644
index 0000000..ce37a5d
--- /dev/null
+++ b/millions/src/main/java/no/ntnu/gruppe53/view/MarketView.java
@@ -0,0 +1,453 @@
+package no.ntnu.gruppe53.view;
+
+import java.math.RoundingMode;
+import java.util.function.Consumer;
+import javafx.collections.transformation.FilteredList;
+import javafx.collections.transformation.SortedList;
+import javafx.geometry.Insets;
+import javafx.geometry.Pos;
+import javafx.scene.chart.LineChart;
+import javafx.scene.chart.XYChart;
+import javafx.scene.control.Button;
+import javafx.scene.control.Label;
+import javafx.scene.control.ListCell;
+import javafx.scene.control.ListView;
+import javafx.scene.control.Spinner;
+import javafx.scene.control.TextField;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.Region;
+import javafx.scene.layout.VBox;
+import javafx.util.Subscription;
+import no.ntnu.gruppe53.model.Exchange;
+import no.ntnu.gruppe53.model.Player;
+import no.ntnu.gruppe53.model.PurchaseCalculator;
+import no.ntnu.gruppe53.model.Share;
+import no.ntnu.gruppe53.model.Stock;
+import no.ntnu.gruppe53.service.FormatBigDecimal;
+import no.ntnu.gruppe53.service.LanguageManager;
+
+/*
+Yellow Color, Hex: #ffe556 RGB(255, 229, 86)
+Blue Color, Hex: #00bcf0 RGB(0, 188, 240)
+Black Color, Hex: #303539 RGB(48, 53, 57)
+ */
+
+/**
+ * Represents a view of the market of an {@link Exchange}, allowing the {@link Player} to see
+ * price history statistics in the form of a {@link LineChart} through the
+ * {@link StockLineChartView} class, as well as buying {@link Stock}s.
+ *
+ *
Extends borderpane to allow easy organizing of the internal layout.
+ *
+ *
Uses a {@link LanguageManager} to set text to the targer language dynamically.
+ */
+public class MarketView extends BorderPane {
+ private final LanguageManager lm = LanguageManager.getInstance();
+
+ private Button purchaseButton;
+ private Label selectedStock;
+
+ // Dynamic Labels/Text
+ public TextField gross;
+ public TextField commission;
+ public TextField total;
+ private TextField searchField;
+
+ // StockListView
+ private ListView stocksListView;
+ private FilteredList filteredStocksList;
+
+ // Input
+ private Spinner quantitySpinner;
+
+ private final StockLineChartView stockLineChartView;
+ private Subscription priceSubscription;
+
+ /**
+ * Initializes the StockLineChartView graph and runs the view
+ * construction method and places it in the center of the
+ * BorderPane.
+ */
+ public MarketView() {
+ stockLineChartView = new StockLineChartView();
+ this.setCenter(centerPane());
+ }
+
+ /**
+ * Constructs the view and its components.
+ *
+ *
Subscribes to a stock's sales price to update the value immediately.
+ *
+ *
Subscribes to the current language to change text immediately.
+ *
+ *
Uses a cell factory to format the string found in the stocksListView.
+ *
+ * @return the constructed MarketView
+ */
+ private VBox centerPane() {
+ VBox vbox = new VBox();
+
+ vbox.setPadding(new Insets(15, 12, 15, 12));
+ vbox.setSpacing(10);
+
+ // Blue Background color
+ vbox.setStyle("-fx-background-color: #00bcf0;");
+
+ Label titleLabel = new Label();
+ titleLabel.textProperty().bind(lm.bindString("marketTitle"));
+ titleLabel.setStyle("-fx-font-size: 20px; -fx-font-weight: bold; ");
+
+ VBox titleBox = new VBox(titleLabel);
+ titleBox.setStyle("-fx-background-color: #ffe556;"
+ + "-fx-text-fill: #303539;"
+ + "-fx-background-radius: 8;"
+ + "-fx-border-color: #303539;"
+ + "-fx-border-radius: 8;"
+ );
+
+ titleBox.setAlignment(Pos.CENTER);
+ titleBox.setPadding(new Insets(4));
+ titleBox.setMaxWidth(200);
+
+ // Stock Graph
+ stockLineChartView.getChart().setMinHeight(0);
+ stockLineChartView.getChart().setMaxHeight(Double.MAX_VALUE);
+ stockLineChartView.getChart().setMinWidth(0);
+ stockLineChartView.getChart().setMaxWidth(Double.MAX_VALUE);
+ stockLineChartView.getChart().getXAxis().labelProperty().bind(lm.bindString("xAxisLabel"));
+ stockLineChartView.getChart().getYAxis().labelProperty().bind(lm.bindString("yAxisLabel"));
+
+ // List of stocks
+ stocksListView = new ListView<>();
+ stocksListView.setMinHeight(0);
+ stocksListView.setMaxHeight(Double.MAX_VALUE);
+ stocksListView.setMinWidth(0);
+ stocksListView.setMaxWidth(Double.MAX_VALUE);
+ stocksListView.setStyle("-fx-background-color: transparent;");
+
+ stocksListView.setCellFactory(list -> new ListCell<>() {
+ private Subscription cellSubscription;
+ private Subscription langSubscription;
+
+ @Override
+ protected void updateItem(Stock stock, boolean empty) {
+ super.updateItem(stock, empty);
+
+ if (cellSubscription != null) {
+ cellSubscription.unsubscribe();
+ cellSubscription = null;
+ }
+
+ if (langSubscription != null) {
+ langSubscription.unsubscribe();
+ langSubscription = null;
+ }
+
+ if (empty || stock == null) {
+ setText(null);
+ } else {
+ Runnable updateTextAction = () -> {
+ String template = lm.getString("stockCellFormat");
+ setText(String.format(template,
+ stock.getSymbol(),
+ stock.getCompany(),
+ FormatBigDecimal.formatNumber(stock.salesPriceProperty().get()),
+ FormatBigDecimal.formatNumber(stock.getLatestPriceChange()),
+ FormatBigDecimal.formatNumber(stock.getHighestPrice()),
+ FormatBigDecimal.formatNumber(stock.getLowestPrice())
+ ));
+ };
+
+ cellSubscription = stock.salesPriceProperty()
+ .subscribe(newPrice -> updateTextAction.run());
+ langSubscription = lm.getBundleProperty()
+ .subscribe(bundle -> updateTextAction.run());
+
+ updateTextAction.run();
+ }
+ }
+ });
+
+ HBox searchBox = new HBox();
+ searchField = new TextField();
+ searchField.promptTextProperty().bind(lm.bindString("search"));
+ searchBox.setStyle("-fx-background-color: #ffe556;"
+ + "-fx-text-fill: #303539;"
+ + "-fx-background-radius: 8;"
+ + "-fx-border-color: #303539;"
+ + "-fx-border-radius: 8;"
+ );
+
+ searchBox.setPadding(new Insets(8, 14, 8, 14));
+ searchBox.setAlignment(Pos.CENTER_LEFT);
+ searchBox.setMaxWidth(Region.USE_PREF_SIZE);
+ searchBox.getChildren().addAll(searchField);
+
+
+ // Container for list of stocks
+ HBox stockListBox = new HBox(stocksListView);
+ stockListBox.setStyle("-fx-background-color: #ffe556;"
+ + "-fx-text-fill: #303539;"
+ + "-fx-background-radius: 8;"
+ + "-fx-border-color: #303539;"
+ + "-fx-border-radius: 8;"
+ );
+
+ // Container for graph of stocks
+ HBox stockChartBox = new HBox(stockLineChartView.getChart());
+ stockChartBox.setStyle("-fx-background-color: #ffe556;"
+ + "-fx-text-fill: #303539;"
+ + "-fx-background-radius: 8;"
+ + "-fx-border-color: #303539;"
+ + "-fx-border-radius: 8;"
+ );
+
+ HBox stockBox = new HBox(stockListBox, stockChartBox);
+ stockBox.setSpacing(10);
+
+ stockListBox.setPadding(new Insets(4));
+ stockListBox.setMinHeight(0);
+ stockListBox.setMaxHeight(Double.MAX_VALUE);
+ stockListBox.setMinWidth(0);
+ stockListBox.setMaxWidth(Double.MAX_VALUE);
+
+ VBox.setVgrow(stocksListView, Priority.ALWAYS);
+ HBox.setHgrow(stocksListView, Priority.ALWAYS);
+ VBox.setVgrow(stockLineChartView.getChart(), Priority.ALWAYS);
+ HBox.setHgrow(stockLineChartView.getChart(), Priority.ALWAYS);
+ VBox.setVgrow(stockListBox, Priority.ALWAYS);
+ HBox.setHgrow(stockListBox, Priority.ALWAYS);
+ VBox.setVgrow(stockChartBox, Priority.ALWAYS);
+ HBox.setHgrow(stockChartBox, Priority.ALWAYS);
+ VBox.setVgrow(stockBox, Priority.ALWAYS);
+ HBox.setHgrow(stockBox, Priority.ALWAYS);
+
+ HBox selectedStockBox = new HBox();
+
+ Label selectedStockLabel = new Label();
+ selectedStockLabel.textProperty().bind(lm.bindString("selectedStock"));
+ selectedStock = new Label("");
+
+ selectedStockBox.getChildren().addAll(selectedStockLabel, selectedStock);
+
+ Label quantityLabel = new Label();
+ quantityLabel.textProperty().bind(lm.bindString("purchaseQuantity"));
+ quantitySpinner = new Spinner<>(1, 1_000_000, 1);
+ quantitySpinner.setEditable(true);
+
+ HBox quantityBox = new HBox();
+ quantityBox.getChildren().addAll(quantityLabel, quantitySpinner);
+
+ Label grossLabel = new Label();
+ grossLabel.textProperty().bind(lm.bindString("purchaseGross"));
+ gross = new TextField();
+ gross.setEditable(false);
+ gross.setMaxWidth(200);
+ Label commissionLabel = new Label();
+ commissionLabel.textProperty().bind(lm.bindString("purchaseCommission"));
+ commission = new TextField();
+ commission.setEditable(false);
+ commission.setMaxWidth(200);
+
+ HBox calculationBox = new HBox();
+ calculationBox.getChildren().addAll(grossLabel, gross, commissionLabel, commission);
+
+ Label totalLabel = new Label();
+ totalLabel.textProperty().bind(lm.bindString("purchaseTotal"));
+ total = new TextField("");
+ total.setEditable(false);
+ total.setMaxWidth(200);
+
+ HBox totalBox = new HBox();
+ totalBox.getChildren().addAll(totalLabel, total);
+
+ purchaseButton = new Button();
+ purchaseButton.textProperty().bind(lm.bindString("purchaseButton"));
+
+ VBox purchaseBox = new VBox(5);
+
+ purchaseBox.setStyle("-fx-background-color: #ffe556;"
+ + "-fx-text-fill: #303539;"
+ + "-fx-background-radius: 8;"
+ + "-fx-border-color: #303539;"
+ + "-fx-border-radius: 8;"
+ );
+
+ purchaseBox.setPadding(new Insets(8, 14, 8, 14));
+ purchaseBox.setAlignment(Pos.CENTER_LEFT);
+
+ purchaseBox.getChildren().addAll(
+ selectedStockBox, quantityBox, calculationBox, totalBox, purchaseButton);
+
+
+
+ vbox.getChildren().addAll(titleBox, searchBox,
+ stockBox,
+ purchaseBox);
+
+ return vbox;
+ }
+
+ /**
+ * Sets and updates subscription to the selected stock's sales price.
+ *
+ * @param stock the selected stock
+ * @param priceChangeAction the action to run on price change (updates the sales price)
+ */
+ public void setupPriceSubscription(Stock stock, Consumer priceChangeAction) {
+ if (priceSubscription != null) {
+ priceSubscription.unsubscribe();
+ priceSubscription = null;
+ }
+
+ if (stock != null) {
+ priceSubscription = stock.salesPriceProperty().subscribe(newPrice -> {
+ priceChangeAction.accept(stock);
+ });
+ }
+ }
+
+ /**
+ * Updates chart data in the StockLineChartView.
+ *
+ * @param series the {@link javafx.scene.chart.XYChart.Series} data for the chart
+ * @param companyName the name of the company of the selected stock
+ */
+ public void updateChart(XYChart.Series series, String companyName) {
+ stockLineChartView.setSeries(series, companyName);
+ }
+
+ /**
+ * Unsubscribes from the previous stocks sales price and clears the LineChart.
+ */
+ public void clearChart() {
+ if (priceSubscription != null) {
+ priceSubscription.unsubscribe();
+ priceSubscription = null;
+ }
+ stockLineChartView.getChart().getData().clear();
+ }
+
+ /**
+ * Sets the action to be run on selecting a stock in stockListView.
+ *
+ * @param action the action to run when selecting a stock
+ */
+ public void onStockSelection(Consumer action) {
+ stocksListView.getSelectionModel().selectedItemProperty().subscribe(action);
+ }
+
+ /**
+ * Sets the action to be run when clicking the purchase button.
+ *
+ * @param action the action to run when clicking the purchase button
+ */
+ public void onPurchaseButton(Runnable action) {
+ purchaseButton.setOnAction(e -> action.run());
+ }
+
+ /**
+ * Sets the action to be run when changing the value of the quantitySpinner.
+ *
+ * @param action the action to run when using the quantitySpinner
+ */
+ public void onQuantitySelect(Runnable action) {
+ quantitySpinner.valueProperty().addListener((obs, oldValue, newValue) -> {
+ action.run();
+ });
+ }
+
+ /**
+ * Sets the exchange to use for the view.
+ *
+ *
Uses first a {@link FilteredList} to account for what the user has searched for.
+ *
+ *
Then utilises a {@link SortedList} to sort the stocks in the exchange alphabetically
+ * on their symbol/ticker.
+ *
+ * @param exchange the exchange to be used for the view
+ */
+ public void setExchange(Exchange exchange) {
+ if (exchange != null) {
+ filteredStocksList = new FilteredList<>(exchange.getObservableStocks());
+
+ SortedList sortedStocks = new SortedList<>(filteredStocksList);
+ sortedStocks.setComparator((s1, s2) -> s1.getSymbol().compareToIgnoreCase(s2.getSymbol()));
+
+ stocksListView.setItems(sortedStocks);
+
+ searchField.textProperty().addListener((observable, oldText, newText) -> {
+ String search = newText == null ? "" : newText.toLowerCase();
+
+ filteredStocksList.setPredicate(stock -> search.isBlank()
+ || stock.getSymbol().toLowerCase().contains(search)
+ || stock.getCompany().toLowerCase().contains(search)
+ );
+ });
+ }
+ }
+
+ /**
+ * Sets the PurchaseCalculator to use for the view.
+ *
+ *
Uses a {@link Subscription} to observe the calculations made by the current
+ * Stock and Quantity selected.
+ *
+ * @param purchaseCalculator the purchaseCalculator to be used for the view
+ */
+ public void setPurchaseCalculator(PurchaseCalculator purchaseCalculator) {
+ Subscription grossSubscription = purchaseCalculator.getGrossProperty().subscribe(newVal -> {
+ if (newVal != null) {
+ gross.setText("$ " + newVal.setScale(2, RoundingMode.HALF_EVEN));
+ } else {
+ gross.setText("");
+ }
+ });
+
+ Subscription commissionSubscription =
+ purchaseCalculator.getCommissionProperty().subscribe(newVal -> {
+ if (newVal != null) {
+ commission.setText("$ " + newVal.setScale(2, RoundingMode.HALF_EVEN));
+ } else {
+ commission.setText("");
+ }
+ });
+
+ Subscription totalSubscription = purchaseCalculator.getTotalProperty().subscribe(newVal -> {
+ if (newVal != null) {
+ total.setText("$ " + newVal.setScale(2, RoundingMode.HALF_EVEN));
+ } else {
+ total.setText("");
+ }
+ });
+ }
+
+ /**
+ * Returns the selected stock in the stocksListView.
+ *
+ * @return the selected stock in the stocksListView
+ */
+ public Stock getSelectedStock() {
+ return stocksListView.getSelectionModel().getSelectedItem();
+ }
+
+ /**
+ * Returns the numeric value of the quantitySpinner for purchasing {@link Share}s in a stock.
+ *
+ * @return an integer of the amount of shares to buy
+ */
+ public int getQuantityPurchase() {
+ return quantitySpinner.getValue();
+ }
+
+ /**
+ * Sets the company name of the selected stock in the stocksListView.
+ *
+ * @param selectedStockString the company name of the selected stock
+ */
+ public void setSelectedStockLabel(String selectedStockString) {
+ selectedStock.setText(selectedStockString);
+ }
+}
diff --git a/millions/src/main/java/no/ntnu/gruppe53/view/NavigationBar.java b/millions/src/main/java/no/ntnu/gruppe53/view/NavigationBar.java
new file mode 100644
index 0000000..838e521
--- /dev/null
+++ b/millions/src/main/java/no/ntnu/gruppe53/view/NavigationBar.java
@@ -0,0 +1,240 @@
+package no.ntnu.gruppe53.view;
+
+import java.math.RoundingMode;
+import javafx.geometry.Insets;
+import javafx.geometry.Pos;
+import javafx.scene.control.Button;
+import javafx.scene.control.Label;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.Region;
+import javafx.util.Subscription;
+import no.ntnu.gruppe53.model.Exchange;
+import no.ntnu.gruppe53.model.Player;
+import no.ntnu.gruppe53.service.LanguageManager;
+
+/*
+Yellow Color, Hex: #ffe556 RGB(255, 229, 86)
+Blue Color, Hex: #00bcf0 RGB(0, 188, 240)
+Black Color, Hex: #303539 RGB(48, 53, 57)
+*/
+
+/**
+ * Represents the top bar of the views.
+ *
+ *
Provides the user with the ability to switch between views on the fly by
+ * clicking on the buttons representing the desired view.
+ *
+ *
The {@link no.ntnu.gruppe53.controller.GameController} handles
+ * the actions for the buttons.
+ *
+ *
Uses a {@link LanguageManager} to set text of UI elements dynamically.
+ */
+public class NavigationBar extends HBox {
+
+ private final Button marketButton;
+ private final Button portfolioButton;
+ private final Button historyButton;
+ private final Button endGameButton;
+ private Subscription netWorthSubscription;
+
+ private Player player;
+
+ // Dynamic Labels
+ private final Label playerLabel;
+ private final Label statusLabel;
+ private final Label netWorthLabel;
+ private final Label moneyLabel;
+
+ /**
+ * Constructs the UI elements of the navigationBar.
+ *
+ *
Binds the language manager to the textProperty of the
+ * UI elements to dynamically change the text.
+ */
+ public NavigationBar() {
+ this.setPadding(new Insets(15, 12, 15, 12));
+ this.setSpacing(10);
+ // Blue Background color
+ this.setStyle("-fx-background-color: #00bcf0;"
+ + "-fx-border-color: transparent transparent #303539 transparent;"
+ + "-fx-border-width: 0 0 5 0;");
+
+ marketButton = new Button();
+ LanguageManager lm = LanguageManager.getInstance();
+ marketButton.textProperty().bind(lm.bindString("marketButton"));
+ marketButton.setStyle("-fx-background-color: #ffe556; -fx-text-fill: #303539;");
+ portfolioButton = new Button();
+ portfolioButton.textProperty().bind(lm.bindString("portfolioButton"));
+ portfolioButton.setStyle("-fx-background-color: #ffe556; -fx-text-fill: #303539;");
+ historyButton = new Button();
+ historyButton.textProperty().bind(lm.bindString("historyButton"));
+ historyButton.setStyle("-fx-background-color: #ffe556; -fx-text-fill: #303539;");
+
+ Region spacer = new Region();
+ HBox.setHgrow(spacer, Priority.ALWAYS);
+
+ HBox playerBox = new HBox();
+
+ Label playerLabelText = new Label();
+ playerLabelText.textProperty().bind(lm.bindString("navPlayerLabelText"));
+ playerLabel = new Label("Player");
+
+ playerBox.setStyle("-fx-background-color: #ffe556;"
+ + "-fx-text-fill: #303539;"
+ + "-fx-background-radius: 8;"
+ + "-fx-border-color: #303539;"
+ + "-fx-border-radius: 8;"
+ );
+
+ playerBox.setPadding(new Insets(8, 14, 8, 14));
+ playerBox.setAlignment(Pos.CENTER);
+
+ playerBox.getChildren().addAll(playerLabelText, playerLabel);
+
+ HBox statusBox = new HBox();
+
+ Label statusLabelText = new Label();
+ statusLabelText.textProperty().bind(lm.bindString("statusLabelText"));
+ statusLabel = new Label();
+
+ statusBox.setStyle("-fx-background-color: #ffe556;"
+ + "-fx-text-fill: #303539;"
+ + "-fx-background-radius: 8;"
+ + "-fx-border-color: #303539;"
+ + "-fx-border-radius: 8;"
+ );
+
+ statusBox.setPadding(new Insets(8, 14, 8, 14));
+ statusBox.setAlignment(Pos.CENTER);
+
+ statusBox.getChildren().addAll(statusLabelText, statusLabel);
+
+ HBox moneyBox = new HBox();
+
+ Label moneyLabelText = new Label();
+ moneyLabelText.textProperty().bind(lm.bindString("navMoneyLabelText"));
+ moneyLabel = new Label();
+
+ moneyBox.setStyle("-fx-background-color: #ffe556;"
+ + "-fx-text-fill: #303539;"
+ + "-fx-background-radius: 8;"
+ + "-fx-border-color: #303539;"
+ + "-fx-border-radius: 8;"
+ );
+
+ moneyBox.setPadding(new Insets(8, 14, 8, 14));
+ moneyBox.setAlignment(Pos.CENTER);
+ moneyBox.getChildren().addAll(moneyLabelText, moneyLabel);
+
+ HBox networthBox = new HBox();
+
+ Label networthLabelText = new Label();
+ networthLabelText.textProperty().bind(lm.bindString("navNetWorthLabelText"));
+ netWorthLabel = new Label();
+
+ networthBox.setStyle("-fx-background-color: #ffe556;"
+ + "-fx-text-fill: #303539;"
+ + "-fx-background-radius: 8;"
+ + "-fx-border-color: #303539;"
+ + "-fx-border-radius: 8;"
+ );
+
+ networthBox.setPadding(new Insets(8, 14, 8, 14));
+ networthBox.setAlignment(Pos.CENTER);
+ networthBox.getChildren().addAll(networthLabelText, netWorthLabel);
+
+ endGameButton = new Button();
+ endGameButton.textProperty().bind(lm.bindString("endGameButton"));
+ endGameButton.setStyle("-fx-background-color: #ffe556; -fx-text-fill: #303539;");
+
+ this.getChildren().addAll(
+ marketButton, portfolioButton, historyButton,
+ spacer, playerBox, statusBox, moneyBox, networthBox, endGameButton);
+ }
+
+ /**
+ * Exposes the marketButton to the GameController.
+ *
+ * @param action the action to be run when clicking the button
+ */
+ public void onMarketButtonClick(Runnable action) {
+ marketButton.setOnAction(e -> action.run());
+ }
+
+ /**
+ * Exposes the portfolioButton to the GameController.
+ *
+ * @param action the action to be run when clicking the button
+ */
+ public void onPortfolioButtonClick(Runnable action) {
+ portfolioButton.setOnAction(e -> action.run());
+ }
+
+ /**
+ * Exposes the historyButton to the GameController.
+ *
+ * @param action the action to be run when clicking the button
+ */
+ public void onHistoryButtonClick(Runnable action) {
+ historyButton.setOnAction(e -> action.run());
+ }
+
+ /**
+ * Exposes the endGameButton to the GameController.
+ *
+ * @param action the action to be run when clicking the button
+ */
+ public void onEndGameButtonClick(Runnable action) {
+ endGameButton.setOnAction(e -> action.run());
+ }
+
+ /**
+ * Binds a subscription/observer to a {@link Player}.
+ *
+ *
It specifically listens to the players money, net worth and name.
+ *
+ * @param player the player object to be observed
+ */
+ public void bindPlayer(Player player) {
+ this.player = player;
+
+ if (netWorthSubscription != null) {
+ netWorthSubscription.unsubscribe();
+ }
+
+ if (player != null) {
+ playerLabel.setText(player.getName());
+ netWorthSubscription = player.getNetWorthProperty().subscribe(newVal -> {
+
+ if (newVal != null) {
+ netWorthLabel.setText("$" + newVal.setScale(2, RoundingMode.HALF_EVEN));
+ } else {
+ netWorthLabel.setText("$0.00");
+ }
+ });
+
+ Subscription moneySubscription = player.getMoneyProperty().subscribe(newVal -> {
+ if (newVal != null) {
+ moneyLabel.setText("$" + newVal.setScale(2, RoundingMode.HALF_EVEN));
+ } else {
+ moneyLabel.setText("$0.00");
+ }
+ });
+
+ }
+ }
+
+ /**
+ * Binds a subscription/observer to a {@link Exchange}.
+ *
+ *
It specifically listens to week changes so that it can update the player status.
+ *
+ * @param exchange the exchange object to be observed
+ */
+ public void bindExchange(Exchange exchange) {
+ Subscription currentWeekSubscription = exchange.getCurrentWeekProperty().subscribe(newVal -> {
+ statusLabel.setText(player.getStatus(exchange));
+ });
+ }
+}
diff --git a/millions/src/main/java/no/ntnu/gruppe53/view/PlayerNameChooserView.java b/millions/src/main/java/no/ntnu/gruppe53/view/PlayerNameChooserView.java
new file mode 100644
index 0000000..046fcb1
--- /dev/null
+++ b/millions/src/main/java/no/ntnu/gruppe53/view/PlayerNameChooserView.java
@@ -0,0 +1,114 @@
+package no.ntnu.gruppe53.view;
+
+import java.util.Optional;
+import javafx.geometry.Insets;
+import javafx.scene.control.Button;
+import javafx.scene.control.ButtonBar;
+import javafx.scene.control.ButtonType;
+import javafx.scene.control.Dialog;
+import javafx.scene.control.Label;
+import javafx.scene.control.TextField;
+import javafx.scene.control.TextFormatter;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.VBox;
+import no.ntnu.gruppe53.model.Player;
+import no.ntnu.gruppe53.service.LanguageManager;
+
+/*
+Yellow Color, Hex: #ffe556 RGB(255, 229, 86)
+Blue Color, Hex: #00bcf0 RGB(0, 188, 240)
+Black Color, Hex: #303539 RGB(48, 53, 57)
+ */
+
+/**
+ * Represents a prompt for the {@link Player}'s name.
+ */
+public class PlayerNameChooserView {
+ private final LanguageManager lm = LanguageManager.getInstance();
+ Dialog dialog;
+
+ /**
+ * Returns the dialog where the player can input a name of maximum 50 characters.
+ *
+ *
A {@link TextFormatter} is used to set the maximum amount
+ * of characters for the player name.
+ *
+ *
Uses a {@link LanguageManager} to determine text language.
+ *
+ * @return the dialog for player name input
+ */
+ public Optional getDialog() {
+ dialog = new Dialog<>();
+
+ // Sets dialog title and header text
+ dialog.setTitle(null);
+ dialog.setHeaderText(null);
+
+ // Sets the dimensions for the dialog pane
+ dialog.setResizable(true);
+
+ dialog.getDialogPane().setPrefWidth(320);
+ dialog.getDialogPane().setPrefHeight(50);
+
+ ButtonType confirmButton =
+ new ButtonType("Confirm", ButtonBar.ButtonData.OK_DONE);
+ dialog.getDialogPane().getButtonTypes().add(confirmButton);
+ Button actualConfirmButton = (Button) dialog.getDialogPane().lookupButton(confirmButton);
+
+ actualConfirmButton.textProperty().bind(lm.bindString("playerNameConfirmButton"));
+
+ TextField nameField = new TextField();
+
+ nameField.setEditable(true);
+
+ int maxLength = 50;
+
+ // Prevents player name over 50 characters
+
+ TextFormatter formatter = new TextFormatter<>(change -> {
+
+ String newText = change.getControlNewText();
+
+ if (newText.length() <= maxLength) {
+ return change;
+ }
+
+ return null;
+
+ });
+
+ nameField.setTextFormatter(formatter);
+
+ // Adds a counter for max characters
+ Label counterLabel = new Label("0 / 50");
+
+ nameField.textProperty().addListener((obs, oldVal, newVal) -> {
+ counterLabel.setText(newVal.length() + " / 50");
+ });
+
+ Label infoLabel = new Label();
+ infoLabel.textProperty().bind(lm.bindString("playerNameInfo"));
+
+ VBox layout = new VBox(15);
+
+ HBox hbox = new HBox(15);
+
+ layout.setPadding(new Insets(10));
+
+ hbox.getChildren().addAll(nameField, counterLabel);
+
+ layout.getChildren().addAll(infoLabel, hbox);
+
+ dialog.getDialogPane().setContent(layout);
+
+ dialog.setResultConverter(button -> {
+ if (button == confirmButton) {
+ return nameField.getText();
+ }
+
+ return null;
+ });
+
+ return dialog.showAndWait();
+ }
+}
\ No newline at end of file
diff --git a/millions/src/main/java/no/ntnu/gruppe53/view/PlayerStartingMoneyChooserView.java b/millions/src/main/java/no/ntnu/gruppe53/view/PlayerStartingMoneyChooserView.java
new file mode 100644
index 0000000..7962367
--- /dev/null
+++ b/millions/src/main/java/no/ntnu/gruppe53/view/PlayerStartingMoneyChooserView.java
@@ -0,0 +1,116 @@
+package no.ntnu.gruppe53.view;
+
+import java.math.BigDecimal;
+import java.util.Optional;
+import javafx.geometry.Insets;
+import javafx.scene.control.Button;
+import javafx.scene.control.ButtonBar;
+import javafx.scene.control.ButtonType;
+import javafx.scene.control.Dialog;
+import javafx.scene.control.Label;
+import javafx.scene.control.Spinner;
+import javafx.scene.control.SpinnerValueFactory;
+import javafx.scene.control.TextFormatter;
+import javafx.scene.layout.VBox;
+import no.ntnu.gruppe53.model.Player;
+import no.ntnu.gruppe53.service.LanguageManager;
+
+/**
+ * Represents the prompt for the {@link Player}'s starting money.
+ *
+ *
Uses a {@link LanguageManager} to set text to target language.
+ */
+public class PlayerStartingMoneyChooserView {
+ Dialog dialog = new Dialog<>();
+ private final LanguageManager lm = LanguageManager.getInstance();
+
+ /**
+ * Returns the dialog for the player's starting money.
+ *
+ *
Uses a {@link TextFormatter} and listener for value integrity.
+ *
+ *
Uses a {@link SpinnerValueFactory} to set min, max, default,
+ * and step size values for the spinner.
+ *
+ * @return the dialog for player's starting money
+ */
+ public Optional getDialog() {
+ // Sets dialog title and header text
+ dialog.setTitle(null);
+ dialog.setHeaderText(null);
+
+ // Sets the dimensions for the dialog pane
+ dialog.setResizable(true);
+
+ dialog.getDialogPane().setPrefWidth(320);
+ dialog.getDialogPane().setPrefHeight(50);
+
+ ButtonType confirmButton =
+ new ButtonType("Confirm", ButtonBar.ButtonData.OK_DONE);
+ dialog.getDialogPane().getButtonTypes().add(confirmButton);
+
+ Button actualConfirmButton = (Button) dialog.getDialogPane().lookupButton(confirmButton);
+
+ actualConfirmButton.textProperty().bind(lm.bindString("playerStartingMoneyConfirmButton"));
+
+ Spinner spinner = new Spinner<>();
+
+ // Sets allowed values and step size for the spinner
+ SpinnerValueFactory.DoubleSpinnerValueFactory valueFactory =
+ new SpinnerValueFactory.DoubleSpinnerValueFactory(
+ 1000.0, // min
+ 1000000.0, // max
+ 5000.0, // initial value
+ 100.0 // step-size
+ );
+
+ spinner.setValueFactory(valueFactory);
+
+ spinner.setEditable(true);
+
+ // Formats the text to only allow numbers, and doubles preceded by a number
+ TextFormatter formatter = new TextFormatter<>(change -> {
+
+ String newText = change.getControlNewText();
+
+ if (newText.matches("\\d+(\\.\\d*)?")) {
+ return change;
+ }
+
+ return null;
+ });
+
+ spinner.focusedProperty().addListener((obs, oldVal, newVal) -> {
+
+ if (!newVal) {
+ spinner.increment(0);
+ }
+ });
+
+ spinner.getEditor().setTextFormatter(formatter);
+
+ spinner.setPrefWidth(200);
+
+ Label infoLabel = new Label();
+ infoLabel.textProperty().bind(lm.bindString("playerStartingMoneyInfoLabel"));
+
+ VBox layout = new VBox(15);
+
+ layout.setPadding(new Insets(10));
+
+ layout.getChildren().addAll(infoLabel, spinner);
+
+ dialog.getDialogPane().setContent(layout);
+
+ dialog.setResultConverter(button -> {
+
+ if (button == confirmButton) {
+ return BigDecimal.valueOf(spinner.getValue());
+ }
+
+ return null;
+ });
+
+ return dialog.showAndWait();
+ }
+}
\ No newline at end of file
diff --git a/millions/src/main/java/no/ntnu/gruppe53/view/PortfolioView.java b/millions/src/main/java/no/ntnu/gruppe53/view/PortfolioView.java
new file mode 100644
index 0000000..416d5c0
--- /dev/null
+++ b/millions/src/main/java/no/ntnu/gruppe53/view/PortfolioView.java
@@ -0,0 +1,410 @@
+package no.ntnu.gruppe53.view;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.function.Consumer;
+import javafx.collections.transformation.FilteredList;
+import javafx.geometry.Insets;
+import javafx.geometry.Pos;
+import javafx.scene.control.Button;
+import javafx.scene.control.Label;
+import javafx.scene.control.ListCell;
+import javafx.scene.control.ListView;
+import javafx.scene.control.TextField;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.Region;
+import javafx.scene.layout.VBox;
+import javafx.scene.text.Text;
+import javafx.scene.text.TextFlow;
+import javafx.util.Subscription;
+import no.ntnu.gruppe53.model.Player;
+import no.ntnu.gruppe53.model.SaleCalculator;
+import no.ntnu.gruppe53.model.Share;
+import no.ntnu.gruppe53.service.FormatBigDecimal;
+import no.ntnu.gruppe53.service.LanguageManager;
+
+/*
+Yellow Color, Hex: #ffe556 RGB(255, 229, 86)
+Blue Color, Hex: #00bcf0 RGB(0, 188, 240)
+Black Color, Hex: #303539 RGB(48, 53, 57)
+ */
+
+/**
+ * Represent a view of the {@link Player}s {@link no.ntnu.gruppe53.model.Portfolio}.
+ *
+ *
Lets the player view their current {@link Share} assets, see their
+ * current sale value, whether the value has gone up or down since they purchased,
+ * and the ability to sell the share for money.
+ *
+ *
Extends the {@link BorderPane}
for easy layout setup.
+ *
+ *
Events are handled by the {@link no.ntnu.gruppe53.controller.GameController}.
+ *
+ *
Uses a {@link LanguageManager} to dynamically set text based on target language.
+ */
+public class PortfolioView extends BorderPane {
+ private final LanguageManager lm = LanguageManager.getInstance();
+
+ private Button sellButton;
+
+ // Dynamic Labels
+ private Label selectedShare;
+ private TextField gross;
+ private TextField commission;
+ private TextField tax;
+ private TextField total;
+ private TextField profit;
+ private TextField searchField;
+
+ // PortfolioListView
+ private ListView portfolioListView;
+ private FilteredList filteredPortfolioList;
+
+ /**
+ * Builds the view components and places them in the center pane.
+ */
+ public PortfolioView() {
+ this.setCenter(centerPane());
+ }
+
+ /**
+ * Constructs the view of the portfolio.
+ *
+ *
Subscribes to shares for sales price updates.
+ *
+ *
Uses a cell factory to format the displayed string representing each share.
+ * The sell factory has the text dynamically set by the language manager.
+ *
+ * @return a VBox of the portfolio view
+ */
+ private VBox centerPane() {
+ VBox vbox = new VBox();
+ vbox.setPadding(new Insets(15, 12, 15, 12));
+ vbox.setSpacing(10);
+ // Blue Background color
+ vbox.setStyle("-fx-background-color: #00bcf0; ");
+
+ Label titleLabel = new Label();
+ titleLabel.textProperty().bind(lm.bindString("portfolioTitle"));
+ titleLabel.setStyle("-fx-font-size: 20px; -fx-font-weight: bold; -fx-text-fill: #303539;");
+
+ VBox titleBox = new VBox(titleLabel);
+ titleBox.setStyle("-fx-background-color: #ffe556;"
+ + "-fx-text-fill: #303539;"
+ + "-fx-background-radius: 8;"
+ + "-fx-border-color: #303539;"
+ + "-fx-border-radius: 8;"
+ );
+
+ titleBox.setAlignment(Pos.CENTER);
+ titleBox.setPadding(new Insets(4));
+ titleBox.setMaxWidth(200);
+
+ portfolioListView = new ListView<>();
+ portfolioListView.setMinHeight(0);
+ portfolioListView.setMaxHeight(Double.MAX_VALUE);
+
+ portfolioListView.setCellFactory(list -> new ListCell<>() {
+ private Subscription cellSubscription;
+ private Subscription langSubscription;
+
+ @Override
+ protected void updateItem(Share item, boolean empty) {
+ super.updateItem(item, empty);
+
+ if (cellSubscription != null) {
+ cellSubscription.unsubscribe();
+ cellSubscription = null;
+ }
+
+ if (langSubscription != null) {
+ langSubscription.unsubscribe();
+ langSubscription = null;
+ }
+
+ if (empty || item == null || item.getStock() == null) {
+ setText(null);
+ setGraphic(null);
+ } else {
+ setText(null);
+
+ Runnable updateGraphicAction = () -> {
+ BigDecimal currentPrice = item.getStock().salesPriceProperty().get();
+ setGraphic(createColoredShareText(item, currentPrice));
+ };
+
+ cellSubscription = item.getStock().salesPriceProperty().subscribe(currentPrice -> {
+ updateGraphicAction.run();
+ });
+
+ langSubscription = lm.getBundleProperty().subscribe(bundle -> {
+ updateGraphicAction.run();
+ });
+
+ updateGraphicAction.run();
+ }
+ }
+ });
+
+ HBox searchBox = new HBox();
+ searchField = new TextField();
+ searchField.promptTextProperty().bind(lm.bindString("search"));
+ searchBox.setStyle("-fx-background-color: #ffe556;"
+ + "-fx-text-fill: #303539;"
+ + "-fx-background-radius: 8;"
+ + "-fx-border-color: #303539;"
+ + "-fx-border-radius: 8;"
+ );
+
+ searchBox.setPadding(new Insets(8, 14, 8, 14));
+ searchBox.setAlignment(Pos.CENTER_LEFT);
+ searchBox.setMaxWidth(Region.USE_PREF_SIZE);
+ searchBox.getChildren().addAll(searchField);
+
+ VBox portfolioListBox = new VBox(portfolioListView);
+ portfolioListBox.setStyle("-fx-background-color: #ffe556;"
+ + "-fx-text-fill: #303539;"
+ + "-fx-background-radius: 8;"
+ + "-fx-border-color: #303539;"
+ + "-fx-border-radius: 8;"
+ );
+
+ portfolioListBox.setPadding(new Insets(4));
+ portfolioListBox.setMinHeight(0);
+ portfolioListBox.setMaxHeight(Double.MAX_VALUE);
+
+ // Ensures the portfoliolist grows to fit the available screenspace
+ VBox.setVgrow(portfolioListView, Priority.ALWAYS);
+ VBox.setVgrow(portfolioListBox, Priority.ALWAYS);
+
+ HBox selectedShareBox = new HBox();
+
+ Label selectedShareLabel = new Label();
+ selectedShareLabel.textProperty().bind(lm.bindString("selectedShare"));
+ selectedShare = new Label();
+
+ selectedShareBox.getChildren().addAll(selectedShareLabel, selectedShare);
+
+ Label grossLabel = new Label();
+ grossLabel.textProperty().bind(lm.bindString("portfolioGross"));
+ gross = new TextField();
+ gross.setEditable(false);
+ gross.setMaxWidth(200);
+ Label commissionLabel = new Label();
+ commissionLabel.textProperty().bind(lm.bindString("portfolioCommission"));
+ commission = new TextField();
+ commission.setEditable(false);
+ commission.setMaxWidth(200);
+ Label taxLabel = new Label();
+ taxLabel.textProperty().bind(lm.bindString("portfolioTax"));
+ tax = new TextField();
+ tax.setEditable(false);
+ tax.setMaxWidth(200);
+
+ HBox calculationBox = new HBox();
+ calculationBox.getChildren().addAll(
+ grossLabel, gross, commissionLabel, commission, taxLabel, tax);
+
+ Label totalLabel = new Label();
+ totalLabel.textProperty().bind(lm.bindString("portfolioTotal"));
+ total = new TextField();
+ total.setEditable(false);
+ total.setMaxWidth(200);
+
+ Label profitLabel = new Label();
+ profitLabel.textProperty().bind(lm.bindString("portfolioProfit"));
+ profit = new TextField();
+ profit.setEditable(false);
+ profit.setMaxWidth(200);
+
+ HBox totalBox = new HBox();
+ totalBox.getChildren().addAll(totalLabel, total, profitLabel, profit);
+
+ sellButton = new Button();
+ sellButton.textProperty().bind(lm.bindString("sellButton"));
+
+ VBox saleBox = new VBox(5);
+
+ saleBox.setStyle("-fx-background-color: #ffe556;"
+ + "-fx-text-fill: #303539;"
+ + "-fx-background-radius: 8;"
+ + "-fx-border-color: #303539;"
+ + "-fx-border-radius: 8;"
+ );
+
+ saleBox.setPadding(new Insets(8, 14, 8, 14));
+ saleBox.setAlignment(Pos.CENTER_LEFT);
+
+ saleBox.getChildren().addAll(selectedShareBox, calculationBox, totalBox, sellButton);
+
+
+ vbox.getChildren().addAll(titleBox, searchBox,
+ portfolioListBox,
+ saleBox);
+ return vbox;
+ }
+
+ /**
+ * Exposes the sell button to the GameController.
+ *
+ * @param action the action to be run when pressing the sell button
+ */
+ public void onSellButton(Runnable action) {
+ sellButton.setOnAction(e -> action.run());
+ }
+
+ /**
+ * Sets the action to be run on selecting a share in portfolioListView.
+ *
+ * @param action the action to run when selecting a share
+ */
+ public void onShareSelection(Consumer action) {
+ portfolioListView.getSelectionModel().selectedItemProperty().subscribe(action);
+ }
+
+ /**
+ * Gets the selected share in the portfolioListView.
+ *
+ * @return the currently selected share
+ */
+ public Share getSelectedShare() {
+ return portfolioListView.getSelectionModel().getSelectedItem();
+ }
+
+ /**
+ * Sets the player to be observed.
+ *
+ *
The values observed are the players portfolio and name.
+ *
+ *
Uses a {@link FilteredList} to get and filter the shares and
+ * binds it to the portfolioListView.
+ *
+ * @param player the player to be observed
+ */
+ public void setPlayer(Player player) {
+ if (player != null && player.getPortfolio() != null) {
+ filteredPortfolioList = new FilteredList<>(player.getPortfolio().getShares());
+
+ portfolioListView.setItems(filteredPortfolioList);
+
+ searchField.textProperty().addListener((observable, oldText, newText) -> {
+ String search = newText == null ? "" : newText.toLowerCase();
+
+ filteredPortfolioList.setPredicate(share -> search.isBlank()
+ || share.getStock().getSymbol().toLowerCase().contains(search)
+ || share.getStock().getCompany().toLowerCase().contains(search)
+ );
+ });
+
+ } else {
+
+ portfolioListView.setItems(null);
+ }
+ }
+
+ /**
+ * Sets the share name of the selected share in the portfolioListView.
+ *
+ * @param selectedShareString the share name of the selected share
+ */
+ public void setSelectedShareLabel(String selectedShareString) {
+ selectedShare.setText(selectedShareString);
+ }
+
+ /**
+ * Sets the SaleCalculator to use for the view.
+ *
+ *
Uses a {@link Subscription} to observe the calculations made by the current
+ * Share selected.
+ *
+ * @param saleCalculator the SaleCalculator to be used for the view
+ */
+ public void setSaleCalculator(SaleCalculator saleCalculator) {
+ Subscription grossSubscription =
+ saleCalculator.getGrossProperty().subscribe(newVal -> {
+ if (newVal != null) {
+ gross.setText("$ " + newVal.setScale(2, RoundingMode.HALF_EVEN));
+ } else {
+ gross.setText("");
+ }
+ });
+
+ Subscription commissionSubscription =
+ saleCalculator.getCommissionProperty().subscribe(newVal -> {
+ if (newVal != null) {
+ commission.setText("$ " + newVal.setScale(2, RoundingMode.HALF_EVEN));
+ } else {
+ commission.setText("");
+ }
+ });
+
+ Subscription taxSubscription = saleCalculator.getTaxProperty().subscribe(newVal -> {
+ if (newVal != null) {
+ tax.setText("$ " + newVal.setScale(2, RoundingMode.HALF_EVEN).abs());
+ } else {
+ tax.setText("");
+ }
+ });
+
+ Subscription totalSubscription = saleCalculator.getTotalProperty().subscribe(newVal -> {
+ if (newVal != null) {
+ total.setText("$ " + newVal.setScale(2, RoundingMode.HALF_EVEN));
+ } else {
+ total.setText("");
+ }
+ });
+
+ Subscription profitSubscription = saleCalculator.getProfitProperty().subscribe(newVal -> {
+ if (newVal != null) {
+ profit.setText("$ " + newVal.setScale(2, RoundingMode.HALF_EVEN));
+ } else {
+ profit.setText("");
+ }
+ });
+ }
+
+ /**
+ * Formats the text representation of the share in the portfolioListView
+ * and applies a color to it based on its current sales price compared
+ * to the buy price of the share.
+ *
+ * @param share the share to be formatted
+ * @param currentPrice current price of the stock the share belongs to
+ * @return a {@link TextFlow} representation of the share
+ */
+ private TextFlow createColoredShareText(Share share, BigDecimal currentPrice) {
+ String text = share.getStock().getSymbol()
+ + " - "
+ + share.getStock().getCompany()
+ + " | "
+ + lm.getString("portfolioShareQuantity")
+ + share.getQuantity()
+ + " | "
+ + lm.getString("portfolioBuyPrice")
+ + "$"
+ + FormatBigDecimal.formatNumber(share.getPurchasePrice())
+ + " | "
+ + lm.getString("portfolioCurrentPrice")
+ + "$";
+
+ Text t1 = new Text(text);
+ Text tvalue = new Text(FormatBigDecimal.formatNumber(currentPrice));
+
+ BigDecimal buyPrice = share.getPurchasePrice();
+ if (buyPrice != null && currentPrice != null) {
+ int compare = currentPrice.compareTo(buyPrice);
+
+ if (compare > 0) {
+ tvalue.setStyle("-fx-fill: #2ecc71; -fx-font-weight: bold;");
+ } else if (compare < 0) {
+ tvalue.setStyle("-fx-fill: #e74c3c; -fx-font-weight: bold;");
+ } else {
+ tvalue.setStyle("-fx-fill: #000000;");
+ }
+ }
+
+ return new TextFlow(t1, tvalue);
+ }
+}
\ No newline at end of file
diff --git a/millions/src/main/java/no/ntnu/gruppe53/view/StartView.java b/millions/src/main/java/no/ntnu/gruppe53/view/StartView.java
new file mode 100644
index 0000000..264fa74
--- /dev/null
+++ b/millions/src/main/java/no/ntnu/gruppe53/view/StartView.java
@@ -0,0 +1,88 @@
+package no.ntnu.gruppe53.view;
+
+import javafx.geometry.Pos;
+import javafx.scene.control.Button;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+import javafx.scene.layout.VBox;
+import javafx.util.Subscription;
+import no.ntnu.gruppe53.controller.StartViewController;
+import no.ntnu.gruppe53.controller.ViewController;
+import no.ntnu.gruppe53.service.LanguageManager;
+
+/**
+ * Represents the first page seen by the user of the application.
+ *
+ *
Uses a {@link LanguageManager} for dynamically setting button and label text.
+ * Subscribes to changes in language set by the manager.
+ */
+public class StartView extends VBox {
+ private final Button newGameButton;
+ private final Button languageButton;
+ private final Button quitButton;
+
+ /**
+ * Constructs the view and sets parameters to be exposed for the {@link StartViewController}.
+ *
+ * @param vm the {@link ViewController} for the view
+ */
+ public StartView(ViewController vm) {
+ this.setAlignment(Pos.CENTER);
+ this.setSpacing(20);
+
+ this.setStyle("-fx-background-color: #00bcf0;");
+
+ Image logo = new Image(getClass().getClassLoader().getResourceAsStream("Millions-logo.png"));
+ ImageView logoView = new ImageView(logo);
+ logoView.setFitWidth(300);
+ logoView.setPreserveRatio(true);
+
+ LanguageManager lm = LanguageManager.getInstance();
+ newGameButton = new Button();
+ newGameButton.textProperty().bind(lm.bindString("newGame"));
+
+ languageButton = new Button();
+ languageButton.setPrefWidth(200);
+ languageButton.setStyle("-fx-background-color: #ffe556; -fx-text-fill: #303539;");
+ Subscription languageSubscription = lm.getBundleProperty().subscribe(bundle -> {
+ languageButton.setText(lm.getString("languageButtonLabel"));
+ });
+
+ quitButton = new Button();
+ quitButton.textProperty().bind(lm.bindString("quitGame"));
+
+ newGameButton.setPrefWidth(200);
+ newGameButton.setStyle("-fx-background-color: #ffe556; -fx-text-fill: #303539;");
+ quitButton.setPrefWidth(200);
+ quitButton.setStyle("-fx-background-color: #ffe556; -fx-text-fill: #303539;");
+
+ this.getChildren().addAll(logoView, newGameButton, languageButton, quitButton);
+ }
+
+ /**
+ * Sets the method to be run when the new game button is pressed.
+ *
+ * @param action the method to execute on button press
+ */
+ public void setOnNewGame(Runnable action) {
+ newGameButton.setOnAction(e -> action.run());
+ }
+
+ /**
+ * Sets the method to be run when the languageButton is pressed.
+ *
+ * @param action the method to execute on button press
+ */
+ public void setOnLanguage(Runnable action) {
+ languageButton.setOnAction(e -> action.run());
+ }
+
+ /**
+ * Sets the method to be run when the quit game button is pressed.
+ *
+ * @param action the method to execute on button press
+ */
+ public void setOnQuit(Runnable action) {
+ quitButton.setOnAction(e -> action.run());
+ }
+}
\ No newline at end of file
diff --git a/millions/src/main/java/no/ntnu/gruppe53/view/StockLineChartView.java b/millions/src/main/java/no/ntnu/gruppe53/view/StockLineChartView.java
new file mode 100644
index 0000000..048d5b9
--- /dev/null
+++ b/millions/src/main/java/no/ntnu/gruppe53/view/StockLineChartView.java
@@ -0,0 +1,128 @@
+package no.ntnu.gruppe53.view;
+
+import java.util.List;
+import javafx.geometry.Side;
+import javafx.scene.chart.LineChart;
+import javafx.scene.chart.NumberAxis;
+import javafx.scene.chart.XYChart;
+import no.ntnu.gruppe53.model.Stock;
+
+/**
+ * Represents a {@link LineChart} view of a {@link Stock}.
+ *
+ *
Y-axis: Price as a {@code double} value. Represented by a {@link NumberAxis}
+ *
+ *
X-axis: Week as an {@code int} value. Represented by a {@code NumberAxis}
+ */
+
+public class StockLineChartView {
+ private final NumberAxis xaxis = new NumberAxis();
+
+ private final LineChart chart;
+
+ /**
+ * Sets the settings for the {@code LineChart}.
+ *
+ *
Sets autoscale, stepsize and manual values for the range x- and y-axis, as well
+ * as the labels.
+ *
+ *
Initializes the LineChart.
+ */
+ public StockLineChartView() {
+ xaxis.setAutoRanging(false);
+ NumberAxis yaxis = new NumberAxis();
+ yaxis.setAutoRanging(true);
+ yaxis.setForceZeroInRange(false);
+
+ xaxis.setLowerBound(1);
+
+ xaxis.setLabel("Week");
+ yaxis.setLabel("Price");
+
+ chart = new LineChart<>(xaxis, yaxis);
+ }
+
+ /**
+ * Returns the constructed LineChart.
+ *
+ * @return the current LineChart
+ */
+ public LineChart getChart() {
+ return chart;
+ }
+
+ /**
+ * Calculates the max x-axis value for a {@link XYChart.Series}.
+ *
+ * @param series the XYChart series to determine max x-axis value from
+ * @return the maximum x-axis value in the XYChart.Series
+ */
+ private double calculatexAxisMax(XYChart.Series series) {
+ return series.getData().stream()
+ .mapToDouble(data -> data.getXValue()
+ .doubleValue())
+ .max()
+ .orElse(0);
+ }
+
+ /**
+ * Calculates the max x-axis value for a {@code List}.
+ *
+ * @param seriesList the list of XYChart.Series to determine max x-axis value from
+ * @return the maximum x-axis value in the List of XYChart.Series
+ */
+ private double calculatexAxisMax(List> seriesList) {
+ double xmax = 0;
+
+ for (XYChart.Series s : seriesList) {
+ double tempMax = calculatexAxisMax(s);
+
+ if (tempMax > xmax) {
+ xmax = tempMax;
+ }
+ }
+
+ return xmax;
+ }
+
+ /**
+ * Sets the {@code XYChart.Series} containing the x- and y-values for the {@code LineChart}.
+ *
+ * @param series the XYChart.Series containing the x- and y-values for the LineChart
+ * @param title the name of the company the stock represents
+ */
+
+ public void setSeries(XYChart.Series series, String title) {
+ chart.getData().clear();
+
+ chart.setTitle(title);
+ chart.setLegendSide(Side.RIGHT);
+
+ double xmax = calculatexAxisMax(series);
+ xaxis.setUpperBound(xmax);
+
+ chart.getData().add(series);
+ }
+
+ /**
+ * Sets the {@code XYChart.Series} containing the x- and y-values for the {@code LineChart}.
+ *
+ *
Here the series values are given as a list of {@code XYChart.Series} objects.
+ *
+ * @param seriesList the list of XYChart.Series containing the x- and y-values for each stock
+ */
+ public void setSeries(List> seriesList) {
+ chart.getData().clear();
+
+ chart.setTitle("Price History");
+ chart.setLegendSide(Side.RIGHT);
+
+ double xax = calculatexAxisMax(seriesList);
+
+ if (xax != 0) {
+ xaxis.setUpperBound(xax);
+ }
+
+ chart.getData().addAll(seriesList);
+ }
+}
\ No newline at end of file
diff --git a/millions/src/main/java/no/ntnu/gruppe53/view/TransactionArchiveView.java b/millions/src/main/java/no/ntnu/gruppe53/view/TransactionArchiveView.java
new file mode 100644
index 0000000..75ea202
--- /dev/null
+++ b/millions/src/main/java/no/ntnu/gruppe53/view/TransactionArchiveView.java
@@ -0,0 +1,235 @@
+package no.ntnu.gruppe53.view;
+
+import java.math.BigDecimal;
+import javafx.collections.transformation.FilteredList;
+import javafx.geometry.Insets;
+import javafx.geometry.Pos;
+import javafx.scene.control.Label;
+import javafx.scene.control.ListCell;
+import javafx.scene.control.ListView;
+import javafx.scene.control.TextField;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.Region;
+import javafx.scene.layout.VBox;
+import javafx.util.Subscription;
+import no.ntnu.gruppe53.model.Player;
+import no.ntnu.gruppe53.model.Purchase;
+import no.ntnu.gruppe53.model.Sale;
+import no.ntnu.gruppe53.model.SaleCalculator;
+import no.ntnu.gruppe53.model.Transaction;
+import no.ntnu.gruppe53.model.TransactionArchive;
+import no.ntnu.gruppe53.service.FormatBigDecimal;
+import no.ntnu.gruppe53.service.LanguageManager;
+
+/*
+Yellow Color, Hex: #ffe556 RGB(255, 229, 86)
+Blue Color, Hex: #00bcf0 RGB(0, 188, 240)
+Black Color, Hex: #303539 RGB(48, 53, 57)
+ */
+
+/**
+ * Represents a view of all the {@link Purchase} and {@link Sale}
+ * {@link Transaction}s a {@link Player} has committed in a game.
+ *
+ *
Extends the {@link BorderPane} for easy layout setup.
+ */
+public class TransactionArchiveView extends BorderPane {
+ private final LanguageManager lm = LanguageManager.getInstance();
+
+ private ListView historyListView;
+ private FilteredList filteredTransactionList;
+
+ private TextField searchField;
+
+ /**
+ * Builds the view components and places them in the center pane.
+ */
+ public TransactionArchiveView() {
+ this.setCenter(centerPane());
+ }
+
+ /**
+ * Constructs the view of the transaction archive.
+ *
+ *
Builds the necessary components and places them in a {@link VBox}.
+ *
+ *
Uses a cell factory to format the string shown
+ *
+ *
Assigns red text to purchases, and green text to sales.
+ *
+ *
Uses the formatBigDecimal method to convert the BigDecimal to a
+ * string with 2 decimals.
+ *
+ *
Uses a {@link LanguageManager} to dynamically set text based on target language.