Skip to content

Commit

Permalink
Merge branch 'main' into 129-final-refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
tommyah committed May 27, 2026
2 parents 4cdcae4 + 0b7352e commit 8a69e9e
Show file tree
Hide file tree
Showing 11 changed files with 188 additions and 63 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -254,11 +254,8 @@ private void applySave(final SaveGame save) {
if (save.getNetWorthHistory() != null && !save.getNetWorthHistory().isEmpty()) {
player.setNetWorthHistory(save.getNetWorthHistory());
} else {
// No recorded history available - seed a minimal two-point
// history so the chart still has something to render.
List<BigDecimal> seed = new ArrayList<>();
seed.add(BigDecimal.valueOf(save.getStartingCapital()));
seed.add(player.getNetWorth());
player.setNetWorthHistory(seed);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.ChoiceBox;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.HBox;
Expand All @@ -15,11 +14,8 @@
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import javafx.util.StringConverter;

import java.io.File;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;

/**
* View shown after the player clicks "Create new game" in the
Expand Down Expand Up @@ -51,8 +47,8 @@ public class CreateGameView extends ViewElement<StackPane, CreateGameActions> {
/** Filename input field. */
private TextField fileNameField;

/** Starting-capital chooser. */
private ChoiceBox<Double> startingCapitalChoice;
/** Starting-capital input. Free-form numeric entry. */
private TextField startingCapitalField;

/** "Bruk default aksje data" button. */
private Button useDefaultStocksButton;
Expand Down Expand Up @@ -112,13 +108,37 @@ public String getFileName() {
}

/**
* Returns the starting capital currently selected, or {@code null}
* if nothing is selected yet.
* Returns the starting capital currently entered, or {@code null}
* if nothing is entered yet or the input is not a valid positive
* number.
*
* @return the selected starting capital, or {@code null}.
* <p>Accepts both "," and "." as decimal separators and ignores
* whitespace, so "10 000", "10000", "10000.5" and "10000,5" all
* parse to the same value.</p>
*
* @return the entered starting capital, or {@code null} if invalid.
*/
public Double getStartingCapital() {
return startingCapitalChoice.getValue();
if (startingCapitalField == null) {
return null;
}
String raw = startingCapitalField.getText();
if (raw == null) {
return null;
}
String cleaned = raw.replace(" ", "").replace(",", ".").trim();
if (cleaned.isEmpty()) {
return null;
}
try {
double parsed = Double.parseDouble(cleaned);
if (parsed <= 0 || Double.isNaN(parsed) || Double.isInfinite(parsed)) {
return null;
}
return parsed;
} catch (NumberFormatException _) {
return null;
}
}

/**
Expand Down Expand Up @@ -179,8 +199,8 @@ public void resetFields() {
if (fileNameField != null) {
fileNameField.clear();
}
if (startingCapitalChoice != null) {
startingCapitalChoice.getSelectionModel().clearSelection();
if (startingCapitalField != null) {
startingCapitalField.clear();
}
this.stockSelection = StockSelection.NONE;
this.customStockFile = null;
Expand Down Expand Up @@ -208,17 +228,10 @@ protected void initLayout() {
// Row 2 - starting capital
Label capitalLabel = new Label("Start capital");
capitalLabel.getStyleClass().add("create-game-label");
startingCapitalChoice = new ChoiceBox<>();
startingCapitalChoice.getItems().addAll(
1_000.0,
10_000.0,
100_000.0,
1_000_000.0,
10_000_000.0
);
startingCapitalChoice.setConverter(buildCapitalConverter());
startingCapitalChoice.getStyleClass().add("create-game-input");
VBox capitalRow = new VBox(6, capitalLabel, startingCapitalChoice);
startingCapitalField = new TextField();
startingCapitalField.setPromptText("Set starting capital (NOK)...");
startingCapitalField.getStyleClass().add("create-game-input");
VBox capitalRow = new VBox(6, capitalLabel, startingCapitalField);

// Row 3 - stock data choice (two buttons side by side)
Label stockLabel = new Label("Stock data");
Expand Down Expand Up @@ -275,7 +288,7 @@ protected void initLayout() {
ChangeListener<Object> enableListener = (obs, oldV, newV) ->
refreshCreateButtonState();
fileNameField.textProperty().addListener(enableListener);
startingCapitalChoice.valueProperty().addListener(enableListener);
startingCapitalField.textProperty().addListener(enableListener);

registerButton(CreateGameActions.USE_DEFAULT_STOCKS,
useDefaultStocksButton);
Expand Down Expand Up @@ -335,27 +348,4 @@ private void refreshCreateButtonState() {
? "create-game-button-enabled"
: "create-game-button-disabled");
}

/**
* Builds a {@link StringConverter} that renders the starting-capital
* values in the choice box using a "1 000 kr"-style format instead
* of the default raw double output.
*/
private StringConverter<Double> buildCapitalConverter() {
DecimalFormatSymbols symbols = new DecimalFormatSymbols();
symbols.setGroupingSeparator(' ');
final DecimalFormat format = new DecimalFormat("#,##0", symbols);
return new StringConverter<>() {
@Override
public String toString(final Double value) {
return value == null ? "" : format.format(value) + " kr";
}

@Override
public Double fromString(final String text) {
// Not used - the choice box is not editable.
return null;
}
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,17 @@ protected void initLayout() {
yAxis.setTickMarkVisible(true);
yAxis.setMinorTickVisible(false);
yAxis.setAutoRanging(false);
yAxis.setLabel("Price (NOK)");
yAxis.setTickLabelFormatter(new javafx.util.StringConverter<Number>() {
@Override
public String toString(final Number value) {
return Math.round(value.floatValue() * 100f) / 100f + " NOK";
}

@Override
public Number fromString(final String string) {
return 0;
}
});

chart = new LineChart<>(xAxis, yAxis);
chart.setCreateSymbols(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ public enum MarketActions {
SORT_TICKER,
/** Sort stocks by current price (descending). */
SORT_PRICE,
/** Sort stocks by latest change (descending). */
/** Cycle the change-filter between winners-only and losers-only. */
SORT_CHANGE,
/** Sort stocks by amount owned (descending). */
/** Show only stocks the player currently owns. */
SORT_OWNED;
}
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,17 @@ protected void initInteractions() {
() -> getViewElement().setSort(MarketSort.TICKER));
getViewElement().setOnAction(MarketActions.SORT_PRICE,
() -> getViewElement().setSort(MarketSort.PRICE));
// The change button cycles between showing only winners and only
// losers - never both at once. The first press defaults to winners
// (the most common "what's going up?" question) and each subsequent
// press flips to the opposite side.
getViewElement().setOnAction(MarketActions.SORT_CHANGE,
() -> getViewElement().setSort(MarketSort.CHANGE));
() -> {
MarketSort next =
(getViewElement().getCurrentSort() == MarketSort.WINNERS)
? MarketSort.LOSERS : MarketSort.WINNERS;
getViewElement().setSort(next);
});
getViewElement().setOnAction(MarketActions.SORT_OWNED,
() -> getViewElement().setSort(MarketSort.OWNED));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,21 @@

/**
* Enum representing the available sorting modes in {@link MarketView}.
*
* <p>{@link #WINNERS} and {@link #LOSERS} are mutually exclusive views
* of the "change" filter button: pressing the button cycles between
* the two. {@link #OWNED} acts as a "show only owned" filter rather
* than a pure sort.</p>
* */
public enum MarketSort {
/** Alphabetical sort by ticker symbol. */
TICKER,
/** Descending sort by current sales price. */
PRICE,
/** Descending sort by latest change percentage. */
CHANGE,
/** Descending sort by amount of shares the player owns. */
/** Show only stocks with positive change, sorted descending. */
WINNERS,
/** Show only stocks with negative change, sorted ascending. */
LOSERS,
/** Show only stocks the player currently owns, sorted by quantity. */
OWNED;
}
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,10 @@ private HBox buildFilterBar() {

sortButtonMap.put(MarketSort.TICKER, sortTickerButton);
sortButtonMap.put(MarketSort.PRICE, sortPriceButton);
sortButtonMap.put(MarketSort.CHANGE, sortChangeButton);
// WINNERS and LOSERS share the same button, the controller cycles
// between them on each press.
sortButtonMap.put(MarketSort.WINNERS, sortChangeButton);
sortButtonMap.put(MarketSort.LOSERS, sortChangeButton);
sortButtonMap.put(MarketSort.OWNED, sortOwnedButton);

registerButton(MarketActions.SORT_TICKER, sortTickerButton);
Expand Down Expand Up @@ -218,6 +221,10 @@ public MarketSort getCurrentSort() {
* Updates the current sort mode, refreshes the active state of the filter
* buttons, and re-renders the stock grid.
*
* <p>The change button doubles as a winners/losers cycle - its label
* updates to reflect the active mode so the user can tell which one
* they're currently looking at.</p>
*
* @param sort the new sort mode.
* */
public void setSort(final MarketSort sort) {
Expand All @@ -229,12 +236,34 @@ public void setSort(final MarketSort sort) {
active.getStyleClass().add("active");
}
}
refreshChangeButtonLabel();
renderStocks();
}

/**
* Updates the label on the change button to reflect which filter mode
* is currently active. The button cycles winners -> losers -> winners
* on press, so showing the active mode makes the cycle discoverable.
* */
private void refreshChangeButtonLabel() {
if (sortChangeButton == null) {
return;
}
sortChangeButton.setText(switch (currentSort) {
case WINNERS -> "winners";
case LOSERS -> "losers";
default -> "change";
});
}

/**
* Rebuilds the stock card grid based on the current search term and
* sort mode.
*
* <p>{@link MarketSort#WINNERS}, {@link MarketSort#LOSERS} and
* {@link MarketSort#OWNED} act as filters: in those modes the grid
* only shows the matching subset of stocks. The other modes show
* every stock that matches the search term.</p>
* */
public void renderStocks() {
if (stocksGrid == null) {
Expand All @@ -247,11 +276,12 @@ public void renderStocks() {
.filter(s ->
s.getSymbol().toLowerCase().contains(term)
|| s.getCompany().toLowerCase().contains(term))
.filter(this::matchesSortFilter)
.sorted(comparatorFor(currentSort))
.toList();

if (filtered.isEmpty()) {
Label empty = new Label("no stocks match your search");
Label empty = new Label(emptyMessageFor(currentSort));
empty.getStyleClass().add("market-empty");
stocksGrid.getChildren().add(empty);
return;
Expand All @@ -262,15 +292,45 @@ public void renderStocks() {
}
}

/**
* Filter predicate that strips out stocks that don't belong in the
* current mode. Sort-only modes (TICKER, PRICE) keep every stock;
* the others filter by change sign or by ownership.
* */
private boolean matchesSortFilter(final Stock stock) {
return switch (currentSort) {
case WINNERS -> changePercent(stock) > 0;
case LOSERS -> changePercent(stock) < 0;
case OWNED -> ownedLookup.applyAsInt(stock.getSymbol()) > 0;
default -> true;
};
}

/**
* Returns the empty-state message that fits the current sort mode,
* so the user knows whether they actually have no matches or whether
* the filter excluded everything.
* */
private String emptyMessageFor(final MarketSort sort) {
return switch (sort) {
case WINNERS -> "no winners right now";
case LOSERS -> "no losers right now";
case OWNED -> "you don't own any stocks yet";
default -> "no stocks match your search";
};
}

/**
* Builds a comparator for the given sort mode.
* */
private Comparator<Stock> comparatorFor(final MarketSort sort) {
return switch (sort) {
case TICKER -> Comparator.comparing(Stock::getSymbol);
case PRICE -> Comparator.comparing(Stock::getSalesPrice).reversed();
case CHANGE -> Comparator.comparingDouble(
case WINNERS -> Comparator.comparingDouble(
(Stock s) -> changePercent(s)).reversed();
case LOSERS -> Comparator.comparingDouble(
(Stock s) -> changePercent(s));
case OWNED -> Comparator.comparingInt(
(Stock s) -> ownedLookup.applyAsInt(s.getSymbol())).reversed();
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ protected void initLayout() {
@Override
protected void initStyling() {
getRootPane().getStyleClass().add("find-stock-minigame-root");
targetLabel.getStyleClass().add("find-stock-minigame-target");
grid.getStyleClass().add("find-stock-minigame-grid");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,6 @@ public <T> void handleEvent(final EventData<T> data) {
balanceHistory.addAll(player.getNetWorthHistory());
} else {
balanceHistory.add(player.getStartingMoney());
balanceHistory.add(player.getNetWorth());
}
pushSnapshot();
}
Expand Down Expand Up @@ -277,7 +276,6 @@ public void handleContextUpdate(final Exchange updatedExchange, final Player upd
this.balanceHistory.addAll(this.player.getNetWorthHistory());
} else {
this.balanceHistory.add(this.player.getStartingMoney());
this.balanceHistory.add(this.player.getNetWorth());
}
pushSnapshot();
}
Expand Down
Loading

0 comments on commit 8a69e9e

Please sign in to comment.