From cfbc5bc1640f32a234eab4af705e97f7400a3470 Mon Sep 17 00:00:00 2001 From: martin Date: Mon, 11 May 2026 14:04:26 +0200 Subject: [PATCH] fet: Adding javafx tab view with graph for stocks --- src/main/java/millions/App.java | 29 ++- .../millions/controller/GameController.java | 46 +++- src/main/java/millions/model/Exchange.java | 8 + .../millions/model/TransactionArchive.java | 4 + src/main/java/millions/view/GameView.java | 203 ++++++++++++++++++ src/main/java/millions/view/StartView.java | 37 +++- 6 files changed, 317 insertions(+), 10 deletions(-) create mode 100644 src/main/java/millions/view/GameView.java diff --git a/src/main/java/millions/App.java b/src/main/java/millions/App.java index 8ffc777..107ec61 100644 --- a/src/main/java/millions/App.java +++ b/src/main/java/millions/App.java @@ -1,14 +1,14 @@ package millions; +import java.math.BigDecimal; import javafx.application.Application; import javafx.scene.Scene; import javafx.stage.Stage; import millions.controller.GameController; +import millions.view.GameView; import millions.view.StartView; -/** - * Main JavaFX application entry point for the Millions stock trading game. - */ +/** Main JavaFX application entry point for the Millions stock trading game. */ public class App extends Application { @Override @@ -16,6 +16,29 @@ public void start(Stage stage) { GameController controller = new GameController(); StartView startView = new StartView(stage); + startView + .getStartButton() + .setOnAction( + event -> { + try { + controller.startGame( + startView.getName(), + new BigDecimal(startView.getStartingAmount()), + startView.getSelectedFile().toPath(), + startView.getPreRunWeeks()); + + GameView gameView = new GameView(controller); + controller.getPlayer().addListener(gameView); + controller.getExchange().addListener(gameView); + + Scene gameScene = new Scene(gameView, 1920, 1080); + stage.setScene(gameScene); + } catch (RuntimeException ex) { + System.err.println(ex); + System.exit(0); + } + }); + Scene scene = new Scene(startView, 400, 350); stage.setTitle("Millions"); stage.setScene(scene); diff --git a/src/main/java/millions/controller/GameController.java b/src/main/java/millions/controller/GameController.java index b6e5561..5638338 100644 --- a/src/main/java/millions/controller/GameController.java +++ b/src/main/java/millions/controller/GameController.java @@ -2,7 +2,9 @@ import java.math.BigDecimal; import java.nio.file.Path; +import java.util.Comparator; import java.util.List; +import java.util.stream.Collectors; import millions.controller.fileIO.CSVStockFileParser; import millions.controller.fileIO.StockFileReader; import millions.model.Exchange; @@ -14,14 +16,23 @@ public class GameController { private Player player; private Exchange exchange; - public void startGame(String name, BigDecimal startingMoney, Path stockFilePath) { + public void startGame( + String name, BigDecimal startingMoney, Path stockFilePath, int preRunWeeks) { + if (preRunWeeks < 0) { + throw new IllegalArgumentException("Pre run weeks cannot be negative"); + } + StockFileReader reader = new StockFileReader(stockFilePath); List lines = reader.readFile(); CSVStockFileParser parser = new CSVStockFileParser(lines); List stocks = parser.parse(); - player = new Player(name, startingMoney); exchange = new Exchange("Exchange", stocks); + for (int i = 0; i < preRunWeeks; i++) { + exchange.advance(); + } + + player = new Player(name, startingMoney); } public Player getPlayer() { @@ -31,4 +42,35 @@ public Player getPlayer() { public Exchange getExchange() { return exchange; } + + public List getStocks() { + return exchange.getStocks().values().stream() + .sorted(Comparator.comparing(Stock::getSymbol)) + .collect(Collectors.toList()); + } + + /** + * Gives alphabetic sort of findStocks + * + * @param searchTerm + * @return + */ + public List searchStocks(String searchTerm) { + if (searchTerm == null || searchTerm.isBlank()) { + return getStocks(); + } + return exchange.findStocks(searchTerm).stream() + .sorted(Comparator.comparing(Stock::getSymbol)) + .collect(Collectors.toList()); + } + + /** + * Get stocks with symbol + * + * @param symbol + * @return + */ + public Stock getStock(String symbol) { + return exchange.getStock(symbol); + } } diff --git a/src/main/java/millions/model/Exchange.java b/src/main/java/millions/model/Exchange.java index 869cff6..ef59694 100644 --- a/src/main/java/millions/model/Exchange.java +++ b/src/main/java/millions/model/Exchange.java @@ -69,6 +69,14 @@ public Transaction sell(Share share, Player player) { return sale; } + public String getName() { + return this.name; + } + + public int getWeekNumber() { + return this.weekNumber; + } + public Map getStocks() { return this.stocks; } diff --git a/src/main/java/millions/model/TransactionArchive.java b/src/main/java/millions/model/TransactionArchive.java index 5ae7f9f..6910a6c 100644 --- a/src/main/java/millions/model/TransactionArchive.java +++ b/src/main/java/millions/model/TransactionArchive.java @@ -25,6 +25,10 @@ public boolean isEmpty() { return transactions.isEmpty(); } + public List getTransactions() { + return new ArrayList<>(transactions); + } + public List getTransactions(int week) { return transactions.stream().filter(x -> x.getWeek() == week).collect(Collectors.toList()); } diff --git a/src/main/java/millions/view/GameView.java b/src/main/java/millions/view/GameView.java new file mode 100644 index 0000000..526c4f3 --- /dev/null +++ b/src/main/java/millions/view/GameView.java @@ -0,0 +1,203 @@ +package millions.view; + +import java.math.BigDecimal; +import java.util.List; +import javafx.scene.chart.LineChart; +import javafx.scene.chart.NumberAxis; +import javafx.scene.chart.XYChart; +import javafx.scene.control.Label; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.control.Tab; +import javafx.scene.control.TabPane; +import javafx.scene.control.TextField; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.VBox; +import millions.controller.GameController; +import millions.model.Exchange; +import millions.model.ExchangeListener; +import millions.model.Player; +import millions.model.PlayerListener; +import millions.model.Stock; +import millions.model.Transaction; + +/** Main game screen with tabs */ +public class GameView extends BorderPane implements PlayerListener, ExchangeListener { + + private final GameController controller; + private final Label playerNameLabel = new Label(); + private final Label weekLabel = new Label(); + private final Label moneyLabel = new Label(); + private final Label netWorthLabel = new Label(); + private final Label statusLabel = new Label(); + + private final TextField searchField = new TextField(); + private final ListView stocksList = new ListView<>(); + private final Label selectedStockLabel = new Label("Select a stock to see chart"); + private final NumberAxis xAxis = new NumberAxis(); + private final NumberAxis yAxis = new NumberAxis(); + private final LineChart stockChart = new LineChart<>(xAxis, yAxis); + + public GameView(GameController controller) { + this.controller = controller; + setTop(createHeader()); + setCenter(createTabs()); + configureStocksList(); + refreshAll(); + } + + private HBox createHeader() { + Label title = new Label("Millions"); + title.setStyle("-fx-font-size: 32px; -fx-font-weight: bold;"); + + HBox header = + new HBox(20, title, playerNameLabel, weekLabel, moneyLabel, netWorthLabel, statusLabel); + return header; + } + + private TabPane createTabs() { + TabPane tabPane = new TabPane(); + tabPane.getTabs().add(createStocksTab()); + tabPane.getTabs().add(createPortfolioTab()); + tabPane.getTabs().add(createTransactionsTab()); + return tabPane; + } + + private Tab createStocksTab() { + VBox leftPane = new VBox(10, new Label("Search"), searchField, stocksList); + + searchField.setPromptText("Search"); + searchField.textProperty().addListener((obs, oldVal, newVal) -> refreshStocks()); + + xAxis.setLabel("Week"); + xAxis.setAutoRanging(false); + xAxis.setLowerBound(1); // Stop week 0 + xAxis.setTickUnit(1); + yAxis.setLabel("Price"); + stockChart.setTitle("Price history"); + stockChart.setLegendVisible(false); + stockChart.setCreateSymbols(true); + stockChart.setAnimated(false); + stockChart.setPrefHeight(500); + + VBox rightPane = new VBox(10, selectedStockLabel, stockChart); + + HBox content = new HBox(12, leftPane, rightPane); + return new Tab("Stocks", content); + } + + private Tab createPortfolioTab() { + VBox content = new VBox(); + return new Tab("Portfolio", content); + } + + private Tab createTransactionsTab() { + VBox content = new VBox(); + return new Tab("Transactions", content); + } + + private void configureStocksList() { + stocksList.setCellFactory( + listView -> + new ListCell<>() { + @Override + protected void updateItem(Stock stock, boolean empty) { + super.updateItem(stock, empty); + if (empty || stock == null) { + setText(null); + } else { + setText(formatStock(stock)); + } + } + }); + + stocksList + .getSelectionModel() + .selectedItemProperty() + .addListener((obs, oldStock, newStock) -> showStockChart(newStock)); + } + + private void refreshAll() { + refreshPlayerInfo(); + refreshStocks(); + } + + private void refreshPlayerInfo() { + Player player = controller.getPlayer(); + Exchange exchange = controller.getExchange(); + + if (player == null || exchange == null) { + return; + } + + playerNameLabel.setText("Player: " + player.getName()); + weekLabel.setText("Week: " + exchange.getWeekNumber()); + moneyLabel.setText("Money: " + player.getMoney()); + netWorthLabel.setText("Net worth: " + player.getNetWorth()); + statusLabel.setText("Status: " + player.getStatus()); + } + + private void refreshStocks() { + List items = controller.searchStocks(searchField.getText()); + stocksList.getItems().setAll(items); + } + + private void showStockChart(Stock stock) { + stockChart.getData().clear(); + + if (stock == null) { + selectedStockLabel.setText("Select a stock to see chart"); + return; + } + + selectedStockLabel.setText( + stock.getSymbol() + + " - " + + stock.getCompany() + + " | Current: " + + stock.getSalesPrice() + + " | High: " + + stock.getHighestPrice() + + " | Low: " + + stock.getLowestPrice()); + + XYChart.Series series = new XYChart.Series<>(); + List prices = stock.getHistoricalPrices(); + xAxis.setUpperBound(Math.max(2, prices.size())); + for (int i = 0; i < prices.size(); i++) { + series.getData().add(new XYChart.Data<>(i + 1, prices.get(i))); + } + stockChart.getData().add(series); + } + + private String formatStock(Stock stock) { + return stock.getSymbol() + " - " + stock.getCompany() + " (" + stock.getSalesPrice() + ")"; + } + + // Listener callbacks update the shared header and the stocks tab. + @Override + public void onMoneyChanged(BigDecimal newBalance) { + refreshPlayerInfo(); + } + + @Override + public void onPortfolioChanged() { + refreshPlayerInfo(); + } + + @Override + public void onStatusChanged(String newStatus) { + refreshPlayerInfo(); + } + + @Override + public void onWeekAdvanced(int newWeek) { + refreshAll(); + } + + @Override + public void onTransactionCompleted(Transaction transaction) { + refreshPlayerInfo(); + } +} diff --git a/src/main/java/millions/view/StartView.java b/src/main/java/millions/view/StartView.java index 0c85a46..ffccb6f 100644 --- a/src/main/java/millions/view/StartView.java +++ b/src/main/java/millions/view/StartView.java @@ -1,6 +1,7 @@ package millions.view; import java.io.File; +import java.math.BigDecimal; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.control.Button; @@ -15,6 +16,7 @@ public class StartView extends VBox { private TextField nameField; private TextField startingAmountField; + private TextField preRunWeeksField; private File selectedFile; private Button filepickerButton; private Button startButton; @@ -24,17 +26,22 @@ public StartView(Stage stage) { setSpacing(12); setPadding(new Insets(40)); - nameField = new TextField(); + nameField = new TextField("user"); nameField.setPromptText("Player name:"); nameField.setMaxWidth(250); nameField.textProperty().addListener((obs, oldVal, newVal) -> checkStartButtonValid()); - - startingAmountField = new TextField(); + // Default to 50000 + startingAmountField = new TextField("50000"); startingAmountField.setPromptText("Starting amount:"); startingAmountField.setMaxWidth(250); startingAmountField .textProperty() .addListener((obs, oldVal, newVal) -> checkStartButtonValid()); + // Pre run weeks to run simulated weeks before the player starts + preRunWeeksField = new TextField("12"); + preRunWeeksField.setPromptText("Pre run weeks:"); + preRunWeeksField.setMaxWidth(250); + preRunWeeksField.textProperty().addListener((obs, oldVal, newVal) -> checkStartButtonValid()); filepickerButton = new Button(); filepickerButton.setText("Pick file"); @@ -49,6 +56,7 @@ public StartView(Stage stage) { if (file != null) { selectedFile = file; filepickerButton.setText(file.getName()); + checkStartButtonValid(); } }); @@ -58,9 +66,12 @@ public StartView(Stage stage) { Label title = new Label("Millions"); title.setStyle("-fx-font-size: 32px; -fx-font-weight: bold;"); - getChildren().addAll(title, nameField, startingAmountField, filepickerButton, startButton); + getChildren() + .addAll( + title, nameField, startingAmountField, preRunWeeksField, filepickerButton, startButton); } + /** Enables/Disables start button */ private void checkStartButtonValid() { boolean valid = true; @@ -68,8 +79,20 @@ private void checkStartButtonValid() { valid = false; } + if (selectedFile == null) { + valid = false; + } + try { - Integer.valueOf(startingAmountField.getText()); + new BigDecimal(startingAmountField.getText()); + } catch (NumberFormatException e) { + valid = false; + } + + try { + if (Integer.parseInt(preRunWeeksField.getText()) < 0) { + valid = false; + } } catch (NumberFormatException e) { valid = false; } @@ -85,6 +108,10 @@ public String getStartingAmount() { return startingAmountField.getText(); } + public int getPreRunWeeks() { + return Integer.parseInt(preRunWeeksField.getText()); + } + public File getSelectedFile() { return selectedFile; }