Skip to content

Commit

Permalink
fixed formating and javadoc
Browse files Browse the repository at this point in the history
  • Loading branch information
peretr committed May 21, 2026
1 parent fd35068 commit 590b658
Show file tree
Hide file tree
Showing 30 changed files with 701 additions and 129 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.
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 @@ -70,13 +70,14 @@ public void initGame(final String username, final Difficulty difficulty,
Path userSelectedPath, PopupsController popupsController) {
player = GameFactory.createPlayer(username, difficulty);
gameController = new GameController();
gameController.startGame();

desktopViewController = new DesktopViewController(player, gameController,
userSelectedPath, 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 @@ -36,8 +36,8 @@ private AudioManager() {
if (stream != null) {
sfxCache.put(audio, stream.readAllBytes());
}
} catch (IOException e) {
System.err.println("[Audio] Failed to cache " + audio + ": " + e.getMessage());
} catch (IOException ignored) {
// Fail silently
}
}
}
Expand Down Expand Up @@ -94,8 +94,8 @@ public void playSfx(final Audio audio) {
});
clip.start();

} catch (Exception e) {
System.err.println("[Audio] SFX playback failed: " + e.getMessage());
} catch (Exception ignored) {
// SFX playback failed silently
}
}, "sfx-thread").start();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,33 @@
* Controller class for managing the stock market exchange.
*/
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 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(Path path) throws IOException, StockFileParseException {
this.stockResolution = 120;
exchange = new Exchange(StockFileHandler.readFromFile(path));
for (Stock stock : exchange.getAllStocks()) {
stockControllers.put(stock.getSymbol(), new StockController(stock));
}
startMarket();
System.out.println("Market loaded");
}

/**
* Returns the exchange managed by this controller.
*
* @return the exchange
*/
public Exchange getExchange() {
return exchange;
}
Expand Down Expand Up @@ -72,7 +80,7 @@ public void startMarket() {
for (Stock stock : exchange.getAllStocks()) {
StockController controller = stockControllers.get(stock.getSymbol());
if (controller != null) {
for (int i = 0; i < stockResolution; i++) {
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
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,9 @@ public StockAppController(

stockApp.getQuantitySpinner().valueProperty().addListener(
(obs, old, newValue) -> {
if (isWeekend) {
if (isWeekend && onMarketClosed != null) {
onMarketClosed.run();
}
}
if (currentStock != null) {
updateReceipt();
}
Expand Down Expand Up @@ -260,8 +260,12 @@ private void handleTransaction() {
if (value > 0) {
try {
handleBuy(currentStock, quantity);
} catch (Exception e) {
onInsufficientFunds.run();
} catch (edu.ntnu.idi.idatt2003.gruppe42.model.exceptions.InsufficientFunds e) {
if (onInsufficientFunds != null) {
onInsufficientFunds.run();
}
} catch (Exception ignored) {
// Log unexpected error silently or handle it
}
} else {
handleSell(currentStock, quantity);
Expand Down Expand Up @@ -289,8 +293,12 @@ private void handleSell(final Stock stock, final BigDecimal quantity) {
Transaction transaction =
marketController.getExchange().sell(existingShare, quantity);
if (transaction != null) {
transaction.commit(player);
player.updateStatus();
try {
transaction.commit(player);
player.updateStatus();
} catch (Exception ignored) {
// Silent fail intended for this specific case
}
}
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,6 @@ public void showMarketClosedWarning() {
*/
public void showInsufficientFundsWarning() {
warningApp.configure(
"NO MORE MONEY!",
"Insufficient Funds",
"You don't have enough cash to complete this purchase.",
"I'll be back"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,8 @@ public DesktopViewController(
}

private List<Popup> initializeApps(GameController gameController) {
AppStoreApp appStoreApp = new AppStoreApp();
gameController.addAppController(new AppStoreController(appStoreApp));
final AppStoreApp appStoreApp = new AppStoreApp();
gameController.addAppController(new AppStoreController());

final HustlersApp hustlersApp = new HustlersApp();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,18 +42,18 @@ public class StartViewController {
private final WarningApp warningApp;

private static final List<String> TATE_NAMES = List.of(
"Top G",
"Cobra Tate",
"Tristan's Brother",
"Emory's Son",
"Matrix Escaper",
"Bugatti Owner",
"Hustler G",
"Chin Checker",
"Sparkling Water Enthusiast",
"The Talisman",
"Baby Oil Hoarder",
"Roofie Supplier"
"Investor G",
"Stock Master",
"Market Pro",
"Financial Wizard",
"Profit Seeker",
"Wealth Builder",
"Capital King",
"Trade Expert",
"Dividend Dreamer",
"Bullish Trader",
"Savvy Investor",
"Money Manager"
);

/**
Expand Down Expand Up @@ -132,8 +132,8 @@ private void handleStart() {
try {
userSelectedPath = Path.of(Objects.requireNonNull(
getClass().getClassLoader().getResource("stocks.csv")).toURI());
} catch (Exception e) {
System.err.println("Failed to load default stocks.csv: " + e.getMessage());
} catch (Exception ignored) {
// Fallback or ignore
}
}

Expand Down
Loading

0 comments on commit 590b658

Please sign in to comment.