Skip to content

fixed gamebraking bugs #142

Merged
merged 4 commits into from
May 25, 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
64 changes: 62 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,62 @@
# Millions
Main repository for group project in IDATT2003 - Programmering 2
# Millions - Stock Trading Simulation

Millions is a JavaFX-based stock trading simulation game where players navigate a dynamic market, manage their portfolio, and progress through trading ranks from Novice to Speculator.

## Features

- **Dynamic Market Simulation**: Stock prices fluctuate based on trends, noise, and scheduled shocks.
- **Progression System**: Gain status levels (Novice → Investor → Speculator) based on your net worth and trading activity.
- **Interactive UI**: A desktop-style interface with multiple "apps" including a Stock Market, Bank, Mail, and Settings.
- **Audio Support**: Immersive background music and sound effects that change based on game state.
- **Data Driven**: Load custom stock data from CSV files.

## Prerequisites

- **Java Development Kit (JDK) 17** or higher.
- **Maven 3.6** or higher.

## Building and Running

### Build the Project
To compile the project and download dependencies, run:
```bash
mvn clean install
```

### Run the Application
To launch the game, use the JavaFX Maven plugin:
```bash
mvn javafx:run
```

### Running Tests
To execute the unit test suite:
```bash
mvn test
```

## Project Architecture

The project follows a modified **Model-View-Controller (MVC)** architectural pattern:

- **Model**: Contains the core business logic and data structures (`Player`, `Stock`, `Exchange`, `Share`, etc.). Models are independent of controllers and views.
- **View**: Handles the visual representation using JavaFX. Components are organized into `views`, `apps`, and `components`.
- **Controller**: Orchestrates the interaction between models and views. `GameController` manages the main loop, while specific app controllers handle individual application logic.

### Key Components

- **EventBus**: A singleton messaging system for decoupled communication between different parts of the application.
- **MarketController**: Manages stock price updates and history generation.
- **StockAppController**: Handles the trading logic, searches, and portfolio updates.
- **TimeAndWeatherController**: Manages in-game time progression and environmental effects.

## Design Decisions

- **Precision**: Used `BigDecimal` for all financial calculations to avoid floating-point errors.
- **Thread Safety**: UI updates from background timers are wrapped in `Platform.runLater()` to ensure consistency with the JavaFX Application Thread.
- **Encapsulation**: Collections returned from models (like `Portfolio.getShares()`) are unmodifiable to prevent accidental external modification.
- **Validation**: Strict input validation in constructors ensures that objects remain in a valid state from creation.

## Credits

Developed as a group project for **IDATT2003 - Programmering 2** at NTNU.
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>6.0.1</version>
<version>5.11.4</version>
</dependency>

</dependencies>
Expand Down
3 changes: 2 additions & 1 deletion src/main/java/edu/ntnu/idi/idatt2003/gruppe42/Millions.java
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,14 @@ public void initGame(final String username, final Difficulty difficulty,
InputStream stocksStream, PopupsController popupsController) {
player = GameFactory.createPlayer(username, difficulty);
gameController = new GameController();
gameController.startGame();

desktopViewController = new DesktopViewController(player, gameController,
stocksStream, popupsController);
desktopViewController.setOnGameOver(this::resetGame);
desktopView = desktopViewController.getDesktopView();
scene.setRoot(desktopView.getRoot());

gameController.startGame();
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
*/
public final class AudioManager {

private static final System.Logger LOGGER =
System.getLogger(AudioManager.class.getName());
private static final AudioManager INSTANCE = new AudioManager();

private final Map<Audio, byte[]> sfxCache = new EnumMap<>(Audio.class);
Expand All @@ -37,7 +39,7 @@ private AudioManager() {
sfxCache.put(audio, stream.readAllBytes());
}
} catch (IOException e) {
System.err.println("[Audio] Failed to cache " + audio + ": " + e.getMessage());
LOGGER.log(System.Logger.Level.WARNING, "Failed to cache SFX: {0}", audio.getPath());
}
}
}
Expand Down Expand Up @@ -94,7 +96,7 @@ public void playSfx(final Audio audio) {
clip.start();

} catch (Exception e) {
System.err.println("[Audio] SFX playback failed: " + e.getMessage());
LOGGER.log(System.Logger.Level.WARNING, "SFX playback failed", e);
}
}, "sfx-thread").start();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,9 @@ public void stopGame() {
/**
* Returns whether the current game state is a weekend.
*
* @return {@code true} if the game state is not {@link GameState#WORKDAY}.
* @return {@code true} if the game state is {@link GameState#FREEDAY}.
*/
public boolean isWeekend() {
return gameState != GameState.WORKDAY;
return gameState == GameState.FREEDAY;
}
}
Original file line number Diff line number Diff line change
@@ -1,38 +1,46 @@
package edu.ntnu.idi.idatt2003.gruppe42.controller;

import edu.ntnu.idi.idatt2003.gruppe42.controller.appcontrollers.AppController;
import edu.ntnu.idi.idatt2003.gruppe42.model.Exchange;
import edu.ntnu.idi.idatt2003.gruppe42.model.Stock;
import edu.ntnu.idi.idatt2003.gruppe42.model.StockFileHandler;
import edu.ntnu.idi.idatt2003.gruppe42.model.exceptions.StockFileParseException;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.Map;

/**
* Controller class for managing the stock market exchange.
*/
public final class MarketController {
public final class MarketController implements AppController {

/** Number of price ticks simulated at startup to pre-populate each stock's price history. */
private static final int STOCK_RESOLUTION = 120;

private Exchange exchange;
private final int stockResolution;
private final Map<String, StockController> stockControllers = new HashMap<>();

/**
* Creates a new market controller and loads stock data from a file.
*
* @param stocksStream the path to the stock data file
* @param path the path to the stock data file
* @throws IOException if the file cannot be read
* @throws StockFileParseException if the file content is malformed
*/
public MarketController(InputStream stocksStream) {
this.stockResolution = 120;
try {
exchange = new Exchange(StockFileHandler.readFromFile(stocksStream));
startMarket();
System.out.println("Market loaded");
} catch (IOException exception) {
System.out.println("File not found");
} catch (StockFileParseException exception) {
System.out.println("Invalid stock file format");
public MarketController(InputStream stocksStream) throws IOException, StockFileParseException {
exchange = new Exchange(StockFileHandler.readFromFile(stocksStream));
for (Stock stock : exchange.getAllStocks()) {
stockControllers.put(stock.getSymbol(), new StockController(stock));
}
startMarket();
}

/**
* Returns the exchange managed by this controller.
*
* @return the exchange
*/
public Exchange getExchange() {
return exchange;
}
Expand All @@ -52,19 +60,29 @@ public List<Stock> getMarket(final String searchTerm) {
*/
public void updateMarket() {
for (Stock stock : exchange.getAllStocks()) {
stock.updatePrice();
stock.updateStockGraph();
StockController controller = stockControllers.get(stock.getSymbol());
if (controller != null) {
stock.addNewSalesPrice(controller.updatePrice());
}
}
}

@Override
public void nextTick() {
updateMarket();
}

/**
* Initializes the market by generating stock history and graphs.
*/
public void startMarket() {
for (Stock stock : exchange.getAllStocks()) {
stock.fakeHistory(stockResolution);
stock.updateStockGraph();

StockController controller = stockControllers.get(stock.getSymbol());
if (controller != null) {
for (int i = 0; i < STOCK_RESOLUTION; i++) {
stock.addNewSalesPrice(controller.updatePrice());
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,16 @@
*/
public class StockController {

/** Threshold for absolute trend value to be considered "steep". */
private static final int STEEP_THRESHOLD = 46;
/** Magnitude multiplier for price shocks during steep trends. */
private static final int EVENT_MAGNITUDE = 90;
/** Maximum range for random price noise added each tick. */
private static final double NOISE_RANGE = 8.0;
/** Minimum number of frames a generated trend must last. */
private static final int MIN_TREND_FRAMES = 10;
/** Number of ticks to delay before applying a pending price shock during a steep trend. */
private static final int SHOCK_DELAY_TICKS = 20;

private final Stock stock;

Expand Down Expand Up @@ -95,7 +101,7 @@ private void nextTrend() {

if (isTrajectorySteep()) {
prepareEvent();
delay = 20;
delay = SHOCK_DELAY_TICKS;
} else {
delay = 0;
pendingShock = null;
Expand Down Expand Up @@ -131,6 +137,11 @@ private boolean isDelay() {
return delay > 0;
}

/**
* Returns the current stock price.
*
* @return the current price
*/
public BigDecimal getPrice() {
return price;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ public class TimeAndWeatherController implements AppController {
private final GameController gameController;
private final AudioManager audioManager = AudioManager.get();

private static final int HOURS_PER_DAY = 24;
private static final int SATURDAY_INDEX = 6;
private static final int SUNDAY_INDEX = 0;
private static final int MIN_TEMPERATURE = -10;
private static final int MAX_TEMPERATURE = 30;

private static final Day[] DAYS = {Day.SUN, Day.MON, Day.TUE, Day.WED, Day.THU, Day.FRI, Day.SAT};

/**
Expand All @@ -43,25 +49,27 @@ public TimeAndWeatherController(GameController gameController) {
*/
@Override
public void nextTick() {
int nextHour = hour.get() + 1;
if (nextHour >= 24) {
dayIndex.set((dayIndex.get() + 1) % 7);
updateGameState();
updateWeather();
hour.set(0);
Platform.runLater(() -> audioManager.playSfx(Audio.CLOCK));
} else {
hour.set(nextHour);
}
Platform.runLater(() -> {
int nextHour = hour.get() + 1;
if (nextHour >= HOURS_PER_DAY) {
dayIndex.set((dayIndex.get() + 1) % 7);
updateGameState();
updateWeather();
hour.set(0);
audioManager.playSfx(Audio.CLOCK);
} else {
hour.set(nextHour);
}
});
}

/**
* Updates the weather and temperature values.
*/
private void updateWeather() {
int change = random.nextInt(11) - 5; // -5 to +5
int change = random.nextInt(11) - 5;
int newTemp = temperature.get() + change;
newTemp = Math.min(Math.max(newTemp, -10), 30);
newTemp = Math.min(Math.max(newTemp, MIN_TEMPERATURE), MAX_TEMPERATURE);
temperature.set(newTemp);


Expand Down Expand Up @@ -157,13 +165,17 @@ public String getWeekString() {
}

/**
* Updates the current game state based on the day.
* Updates the current game state based on the day index.
*
* <p>Saturday (index 6) → {@link GameState#RENTDAY};<br>
* Sunday (index 0) → {@link GameState#FREEDAY};<br>
* all other days → {@link GameState#WORKDAY}.
*/
public void updateGameState() {
if (dayIndex.get() == 6) {
if (dayIndex.get() == SATURDAY_INDEX) {
gameController.setGameState(GameState.RENTDAY);
return;
} else if (dayIndex.get() == 0) {
} else if (dayIndex.get() == SUNDAY_INDEX) {
gameController.setGameState(GameState.FREEDAY);
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,8 @@ public class AppStoreController implements AppController {

/**
* Constructs the App Store controller.
*
* @param appStoreApp the App Store view component
*/
public AppStoreController(final AppStoreApp appStoreApp) {
public AppStoreController() {
}

/**
Expand Down
Loading