Skip to content

Commit

Permalink
Merge pull request #142 from einaskoi/per/bugFixes
Browse files Browse the repository at this point in the history
fixed gamebraking bugs
  • Loading branch information
einaskoi authored May 25, 2026
2 parents c496c08 + 0854405 commit afaf17e
Show file tree
Hide file tree
Showing 41 changed files with 954 additions and 253 deletions.
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

0 comments on commit afaf17e

Please sign in to comment.