Skip to content

fikset??? #151

Merged
merged 1 commit into from
May 26, 2026
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Player.java
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,23 @@ public void setNetWorthHistory(final List<BigDecimal> history) {
}
}

/**
* Appends a new net-worth sample to this player's history.
*
* <p>Used by the simulation driver to record one data point per
* advanced week so the resulting time series can be persisted into a
* {@link SaveGame} and replayed by the stats dashboard after a
* reload. Null values are ignored.</p>
*
* @param sample the net-worth value to record.
*/
public void recordNetWorthSample(final BigDecimal sample) {
if (sample == null) {
return;
}
this.netWorthHistory.add(sample);
}

/**
* Adds money to the players balance.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,29 +175,54 @@ public <T> void invoke(final EventData<T> data, final EventManager manager) {
* @param save save to apply.
* */
private void applySave(final SaveGame save) {
Map<String, BigDecimal> activeBaseline;
if (save.getExchangeStocks() != null && !save.getExchangeStocks().isEmpty()) {
activeBaseline = captureBaseline(save.getExchangeStocks());
// The save snapshot now carries the full price history for every
// stock, so the simplest and most deterministic restore is to drop
// the lagged exchange's stock pool entirely and replace it with the
// saved one. That preserves both the current price *and* the
// historical chart line, with no random replay between save and
// load.
//
// For old save files that only contain a current price per stock
// (no "prices" array, no "netWorthHistory" array), we still need a
// sensible fallback. In that case we keep the previous behaviour
// of resetting the live exchange to the baseline and advancing the
// simulation forward week-by-week, accepting that the resulting
// chart will be "freshly simulated" because the source data simply
// doesn't contain the history needed to be more accurate.
boolean haveSavedStocks =
save.getExchangeStocks() != null && !save.getExchangeStocks().isEmpty();
boolean haveSavedHistory = haveSavedStocks
&& save.getExchangeStocks().getFirst().getHistoricalPrices().size() > 1;

if (haveSavedStocks && haveSavedHistory) {
// Modern save: snapshot has full price history per stock.
// Replace pool wholesale, set the week directly, do NOT advance.
this.exchange.updateStockPool(save.getExchangeStocks());
exchange.setWeek(Math.max(1, save.getWeek()));
} else {
activeBaseline = this.baselinePrices;
}
// Legacy save: no per-stock history available. Reset to a
// baseline and advance the simulation forward to the saved week
// so the rest of the game state at least has the right number
// of price ticks behind it.
Map<String, BigDecimal> activeBaseline = haveSavedStocks
? captureBaseline(save.getExchangeStocks())
: this.baselinePrices;
if (haveSavedStocks) {
this.exchange.updateStockPool(save.getExchangeStocks());
}
exchange.resetStocksTo(activeBaseline);
exchange.setWeek(1);

exchange.resetStocksTo(activeBaseline);
exchange.setWeek(1);
int targetWeek = Math.max(1, save.getWeek());
while (exchange.getWeek() < targetWeek) {
exchange.advance();
}
}

Portfolio portfolio = player.getPortfolio();
portfolio.clear();
TransactionArchive archive = player.getTransactionArchive();
archive.clear();
player.setMoney(BigDecimal.valueOf(save.getStartingCapital()));
player.refreshProperties();

int targetWeek = Math.max(1, save.getWeek());
while (exchange.getWeek() < targetWeek) {
exchange.advance();
}

for (OwnedShareData od : save.getOwnedShares()) {
if (!exchange.hasStock(od.getSymbol())) {
Expand Down Expand Up @@ -226,10 +251,15 @@ private void applySave(final SaveGame save) {

player.setMoney(BigDecimal.valueOf(save.getBalance()));

if (save.getNetWorthHistory() != null) {
if (save.getNetWorthHistory() != null && !save.getNetWorthHistory().isEmpty()) {
player.setNetWorthHistory(save.getNetWorthHistory());
} else {
player.setNetWorthHistory(new ArrayList<>());
// 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);
}

player.refreshProperties();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -255,16 +255,51 @@ private SaveGame parseFile(final Path file) {

if (sym != null && nm != null && prcStr != null) {
try {
exchangeStocks.add(new Stock(sym, nm, new BigDecimal(prcStr)));
// Read full price history if present, otherwise fall back
// to the single "price" field for backwards compatibility
// with old save files.
List<BigDecimal> history =
parseNumberArray(extractArrayBody(objectStr, "prices"));
BigDecimal seed;
if (history != null && !history.isEmpty()) {
seed = history.getFirst();
} else {
seed = new BigDecimal(prcStr);
}

Stock stock = new Stock(sym, nm, seed);
if (history != null && history.size() > 1) {
for (int idx = 1; idx < history.size(); idx++) {
stock.addNewSalesPrice(history.get(idx));
}
}

// Restore fortune if present.
String fortuneStr = stockFields.get("fortune");
if (fortuneStr != null && !fortuneStr.isEmpty()) {
try {
stock.setFortune(Double.parseDouble(fortuneStr));
} catch (NumberFormatException ignored) {
// Default fortune of 0 will be retained.
}
}

exchangeStocks.add(stock);
} catch (Exception e) {
// Skip individual invalid elements.
}
}
}
}

List<BigDecimal> netWorthHistory =
parseNumberArray(extractArrayBody(content, "netWorthHistory"));
if (netWorthHistory == null) {
netWorthHistory = Collections.emptyList();
}

return new SaveGame(name, balance, startingCapital, stockDataPath,
week, ownedShares, transactions, exchangeStocks);
week, ownedShares, transactions, exchangeStocks, netWorthHistory);
} catch (IOException | NumberFormatException e) {
System.err.println("Skipping invalid save file "
+ file.getFileName() + ": " + e.getMessage());
Expand Down Expand Up @@ -301,11 +336,21 @@ private String toJson(final SaveGame save) {
sb.append(" \"ownedShares\": ").append(ownedSharesToJson(save.getOwnedShares())).append(",\n");
sb.append(" \"transactions\": ").append(transactionsToJson(save.getTransactions())).append(",\n");

sb.append(" \"stocks\": ").append(stocksToJson(save.getExchangeStocks())).append("\n");
sb.append(" \"stocks\": ").append(stocksToJson(save.getExchangeStocks())).append(",\n");
sb.append(" \"netWorthHistory\": ").append(numberListToJson(save.getNetWorthHistory())).append("\n");
sb.append("}\n");
return sb.toString();
}

/**
* Serialises a list of {@link Stock} entries to a JSON array.
*
* <p>Each stock writes its current price <em>and</em> its full price history
* so the per-stock charts remain consistent across save/load cycles. The
* {@code price} field is kept for backwards compatibility with old saves
* and external readers; loaders that recognise {@code prices} should
* prefer the array.</p>
*/
private String stocksToJson(final List<Stock> stocks) {
if (stocks == null || stocks.isEmpty()) {
return "[]";
Expand All @@ -317,6 +362,8 @@ private String stocksToJson(final List<Stock> stocks) {
sb.append(" { \"symbol\": ").append(quote(s.getSymbol()))
.append(", \"name\": ").append(quote(s.getCompany()))
.append(", \"price\": ").append(s.getSalesPrice().toPlainString())
.append(", \"prices\": ").append(numberListToJson(s.getHistoricalPrices()))
.append(", \"fortune\": ").append(BigDecimal.valueOf(s.getFortune()).toPlainString())
.append(" }");
if (i < stocks.size() - 1) {
sb.append(",");
Expand All @@ -327,6 +374,29 @@ private String stocksToJson(final List<Stock> stocks) {
return sb.toString();
}

/**
* Serialises a list of {@link BigDecimal} values to a compact JSON array.
*
* <p>Returns {@code "[]"} for null/empty lists. Uses {@link BigDecimal#toPlainString()}
* to avoid scientific notation so the file remains diff-friendly and
* round-trippable.</p>
*/
private String numberListToJson(final List<BigDecimal> values) {
if (values == null || values.isEmpty()) {
return "[]";
}
StringBuilder sb = new StringBuilder();
sb.append("[");
for (int i = 0; i < values.size(); i++) {
if (i > 0) {
sb.append(", ");
}
sb.append(values.get(i).toPlainString());
}
sb.append("]");
return sb.toString();
}

/**
* Serialises a list of {@link OwnedShareData} entries to a JSON array.
*
Expand Down Expand Up @@ -561,6 +631,44 @@ private List<OwnedShareData> parseOwnedSharesArray(final String body) {
return result;
}

/**
* Parses the body of a flat JSON number array into a list of
* {@link BigDecimal} values.
*
* <p>Accepts a comma-separated list of numeric tokens (with optional
* surrounding whitespace and an optional minus sign). Returns
* {@code null} when the supplied body is {@code null} (so callers can
* distinguish "field absent" from "field empty"). Returns an empty
* list when the body is empty or contains only whitespace. Tokens
* that fail {@link BigDecimal} parsing are silently skipped so a
* single malformed entry doesn't break the rest of the array.</p>
*
* @param body the raw substring between {@code [} and {@code ]}, or {@code null}.
* @return parsed list of {@link BigDecimal} values, or {@code null} if {@code body} is null.
*/
private List<BigDecimal> parseNumberArray(final String body) {
if (body == null) {
return null;
}
List<BigDecimal> result = new ArrayList<>();
String trimmed = body.trim();
if (trimmed.isEmpty()) {
return result;
}
for (String raw : trimmed.split(",")) {
String token = raw.trim();
if (token.isEmpty()) {
continue;
}
try {
result.add(new BigDecimal(token));
} catch (NumberFormatException e) {
System.err.println("Skipping malformed numeric entry: " + token);
}
}
return result;
}

/**
* Parses the body of a {@code transactions} array into a list of
* {@link TransactionData}.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,24 @@ protected void initInteractions() {
// constructor, so we must initialize the list here.
balanceHistory = new ArrayList<>();

// Seed the history with the players starting balance so the chart
// and the all-time P&L KPI have a meaningful baseline from week 1.
balanceHistory.add(player.getStartingMoney());
// Seed the chart history from the players recorded net-worth
// history if present (eg. after a save load), otherwise from
// starting money so the chart and the all-time P&L KPI have a
// meaningful baseline from week 1.
if (player.getNetWorthHistory() != null && !player.getNetWorthHistory().isEmpty()) {
balanceHistory.addAll(player.getNetWorthHistory());
} else {
balanceHistory.add(player.getStartingMoney());
}
pushSnapshot();

exchange.weekProperty().addListener((observable, o, n) -> {
balanceHistory.add(player.getNetWorth());
BigDecimal sample = player.getNetWorth();
balanceHistory.add(sample);
// Also record the sample on the player so the snapshot taken by
// GameStateLoader (and persisted by SaveGameService) captures the
// full time series for the next load.
player.recordNetWorthSample(sample);
pushSnapshot();
});

Expand Down