Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 60

Managing Your Income and Expenses

Recommended resources

Hello! Welcome back to Module 10 of our course.

In the previous lesson, we established the foundation of our game's economy by implementing an encapsulated Wallet for our characters. They now have a way to store and manage money safely. However, a wallet is only useful if money moves in and out of it.

This lesson builds directly on that foundation. We will bring the economy to life by implementing two core recurring financial events: a basic income source and recurring bills. This will create a simple but essential economic loop that influences a character's life.

By the end of this lesson, you will have implemented a system that automatically gives characters a daily stipend and charges them for weekly bills, all driven by the in-game clock.

1. The Concept of a Scheduler

At its core, our task is to make specific actions happen at regular intervals: "add money every day" and "subtract money every week." In software development, this type of problem is handled by a scheduler. A scheduler is a component responsible for executing predefined jobs according to a set schedule.

While the scheduler we build will be tailored for game time, the general concept is universal. To get a clear idea of the principles, the following article discusses building a scheduler for a web application.

Building a scheduler for a Rust application

This article, 'Building a scheduler for a Rust application', explains the core ideas behind scheduling recurring tasks. While it focuses on a real-time web context, the fundamental principles of defining and managing jobs are highly relevant to our goal.

Read the introduction and the 'Flexibility' section. Pay close attention to: The explanation of why a scheduler is a vital component for handling regular, automated tasks. The concept of defining jobs with custom schedules, like 'once a day' or 'midnight every Saturday'. This is the exact pattern we need for our stipend and bills.

As the article highlights, schedulers provide a clean and powerful way to handle recurring logic. A key difference for us is that libraries like tokio-cron-scheduler are tied to the real-world clock. Our game has its own internal clock (from Module 5), which might run much faster than real time. Therefore, we will implement a simple scheduler that hooks directly into our game's time progression.


2. Designing a Simple Economy Manager

Instead of a complex, generic job scheduler, we can create a dedicated EconomyManager that is perfectly suited to our current needs. This manager will track the passage of game time and trigger economic events accordingly. This approach is straightforward, efficient, and easy to integrate with our existing GameClock.

Our design will be:

  1. An EconomyManager struct that holds the configuration for stipends and bills (e.g., their amounts).
  2. The struct will also keep track of the last game day a stipend was paid and the last game week bills were processed.
  3. We'll create an update method on the EconomyManager. This method will be called on every tick of the main game loop.
  4. Inside update, we'll check the current game time. If a new day or week has passed since the last event, we will process the payments for all characters.

This design is simple, aligns with your preference for a "build-as-you-learn" approach, and directly solves the learning outcome.


3. Implementing the EconomyManager in Rust

Let's translate this design into code. We'll create the EconomyManager and then integrate it into our main game state.

Step 3.1: Define the Struct and its Core Logic

First, let's create the EconomyManager struct. You can place this in a new src/economy.rs file and make it a module, or add it to your main game state file.

We need to make an assumption here: your GameClock struct from Module 5 provides methods like get_current_day() and get_current_week() that return integers representing the total number of days and weeks passed since the game started.

use serde::{Serialize, Deserialize};
// Make sure to import your Character and GameClock structs
// e.g., use crate::character::Character;
// e.g., use crate::clock::GameClock;




#[derive(Serialize, Deserialize, Debug)]
pub struct EconomyManager {
    daily_stipend_amount: f64,
    weekly_bills_amount: f64,
    
    // Tracks the last game-day a stipend was paid
    last_stipend_day: u32,
    // Tracks the last game-week bills were charged
    last_bills_week: u32,
}

impl EconomyManager {
    /// Creates a new EconomyManager with specified amounts for income and expenses.
    pub fn new(stipend: f64, bills: f64) -> Self {
        EconomyManager {
            daily_stipend_amount: stipend,
            weekly_bills_amount: bills,
            // Initialize to 0, so events trigger on day/week 1
            last_stipend_day: 0,
            last_bills_week: 0,
        }
    }

    /// This method should be called once per game tick to process economic events.
    pub fn update(&mut self, game_clock: &GameClock, characters: &mut Vec<Character>) {
        let current_day = game_clock.get_current_day();
        let current_week = game_clock.get_current_week();

        // --- Process Daily Stipend ---
        if current_day > self.last_stipend_day {
            println!("[Economy] Processing daily stipend for Day {}.", current_day);
            for character in characters.iter_mut() {
                character.wallet.deposit(self.daily_stipend_amount);
            }
            // Update the last run day to the current day
            self.last_stipend_day = current_day;
        }

        // --- Process Weekly Bills ---
        if current_week > self.last_bills_week {
            println!("[Economy] Processing weekly bills for Week {}.", current_week);
            for character in characters.iter_mut() {
                // We use a match to handle the Result from withdraw()
                match character.wallet.withdraw(self.weekly_bills_amount) {
                    Ok(_) => {
                        // Successfully paid bills, no action needed.
                    },
                    Err(e) => {
                        // This character couldn't afford their bills!
                        // This is a great place for future gameplay hooks like
                        // negative moodlets, debt accumulation, or notifications.
                        println!("[Economy] WARNING: {} could not pay bills: {}", character.name, e);
                    }
                }
            }
            // Update the last run week to the current week
            self.last_bills_week = current_week;
        }
    }
}

Notice how the update function is stateless regarding its inputs; it relies on its own internal state (last_stipend_day, last_bills_week) and the GameClock to decide when to act. This makes the logic very predictable.

Step 3.2: Integrate into the Game State and Loop

Now, we need to add the EconomyManager to your main GameState struct and call its update method from your game loop.

  1. Add EconomyManager to GameState:
    Find your primary GameState struct that holds all simulation data and add the manager to it.

    // In your main state definition file
    // ... other use statements
    use crate::economy::EconomyManager;
    
    
    
    
    #[derive(Serialize, Deserialize, Debug)]
    pub struct GameState {
        // ... other fields like characters, world, clock ...
        pub clock: GameClock,
        pub characters: Vec<Character>,
        pub economy_manager: EconomyManager,
    }
    
    impl GameState {
        pub fn new() -> Self {
            // ... creation of other state ...
            GameState {
                // ...
                characters: vec![/* ... your initial characters ... */],
                clock: GameClock::new(),
                // Initialize the economy with, for example, a $100 stipend and $250 in bills
                economy_manager: EconomyManager::new(100.0, 250.0), 
            }
        }
    }
    
  2. Call update from the Game Loop:
    In your main game loop where you update the GameClock and character needs, add a call to the EconomyManager's update method.

    // In your main game loop function
    fn update_game_state(state: &mut GameState) {
        // 1. Update the clock
        state.clock.tick();
        
        // 2. Update character needs (from Module 7)
        // ... needs_manager.update(&state.clock, &mut state.characters) ...
        
        // 3. NEW: Update the economy
        state.economy_manager.update(&state.clock, &mut state.characters);
    
        // ... other update logic ...
    }
    

4. Testing Your Economic Engine

With the code in place, it's time to test it.

  1. Make sure your println! statements are present in the EconomyManager::update method.
  2. Run your game.
  3. Set the game speed to fast-forward.
  4. Watch the console output. You should see the [Economy] Processing daily stipend... message appear once per game day, and the [Economy] Processing weekly bills... message once per game week.
  5. If you have a UI that displays a character's wallet balance, you should see it increase daily and then take a larger hit once a week. If not, you can add temporary println! statements to log a character's balance after each event to verify the changes.

This confirms that your economic events are correctly scheduled and executed based on the passage of in-game time.


Conclusion

Congratulations! You have successfully implemented a dynamic economic system. Characters now have a steady income and recurring expenses, creating a fundamental gameplay loop of earning and spending. This system automatically manages the flow of money in your simulation world.

Key Takeaways:

  • We implemented a basic economic cycle using the scheduler pattern, where jobs (stipend, bills) are triggered based on a schedule (daily, weekly).
  • Our EconomyManager provides a clean, dedicated home for this logic, making it easy to manage and expand.
  • The system is driven by the in-game GameClock, ensuring economic events are synchronized with the simulation's time, not real-world time.
  • By handling the Err case from the withdraw method, we've already created a hook for future gameplay mechanics related to financial hardship.

Preview of the Next Lesson:
Characters now have money and a reason to manage it. The next logical step is to give them something to spend it on. In the next lesson, we will begin building the "Buy Mode" by defining a JSON schema for purchasable objects, complete with properties like cost and their effect on character needs. This will connect our new economy system directly to player interaction and object placement.