Part 4: Adding Dynamics - More Control Flow & Asynchrony

Welcome back, future Dart expert! You're doing great. So far, you've mastered the basics of the main function, handling user input, and organizing your code with classes and methods. Our digital pet now has a name and displays its status clearly. In this part, we're going to make our digital pet truly interactive. We'll introduce a continuous game loop, allow the user to choose actions using a switch statement, and dive into the fascinating world of asynchronous programming so our pet's actions can "take time" without freezing our program.


Recap from Part 3 (Your Code So Far)

At the end of Part 3, your main.dart file should look like this:

// main.dart

import 'dart:io';

class DigitalPet {
  String name;
  int _hunger;
  int _happiness;

  static const int MAX_STAT_VALUE = 10; // Maximum value for stats
  static const int MIN_STAT_VALUE = 0; // Added in Part 4, but good to include for reference here

  DigitalPet(this.name, this._hunger, this._happiness);

  // Public getters to access the private hunger and happiness
  int get hunger => _hunger;
  int get happiness => _happiness;

  void displayStatus() {
    print('\n--- ${this.name}\'s Status ---');
    print('Hunger: [${_getStatusBar(_hunger)}]');
    print('Happiness: [${_getStatusBar(_happiness)}]');
    print('-------------------------');
  }

  String _getStatusBar(int value) {
    String bar = '';
    for (int i = 0; i < MAX_STAT_VALUE; i++) {
      if (i < value) {
        bar += '#';
      } else {
        bar += '-';
      }
    }
    return bar;
  }
}


void main() {
  print('Welcome to Digital Pet Simulator!');
  stdout.write('What would you like to name your pet? ');
  String? petName = stdin.readLineSync();

  if (petName == null || petName.trim().isEmpty) {
    petName = 'Buddy';
    print('No name entered, calling your pet $petName!');
  } else {
    petName = petName.trim();
    print('Hello, $petName!');
  }

  DigitalPet myPet = DigitalPet(petName!, 5, 5); // Initializing with private hunger/happiness
  myPet.displayStatus();
}

If you run this (dart run main.dart), it asks for a name, displays the initial status, and then exits. We want it to keep running!


Step 1: The Main Game Loop (while statement)

To make our program continuous, we'll use a while loop. A while loop repeatedly executes a block of code as long as a given condition is true. We'll use while (true) to create an infinite loop that will keep our game running until we explicitly tell it to stop.

Your Task:

// Inside your DigitalPet class:
class DigitalPet {
  // ...
  static const int MAX_STAT_VALUE = 10;
  static const int MIN_STAT_VALUE = 0; // New constant!
  // ...
}
void main() {
  // ... (existing code for pet name and creation) ...

  DigitalPet myPet = DigitalPet(petName!, 5, 5);

  // New: The main game loop!
  while (true) { // This loop will run forever until we exit explicitly
    myPet.displayStatus();

    // (We'll add user menu and choices here next)

    // For now, let's just make it exit after one loop to test
    // Later, we'll remove this 'return;'
    return; // Remove this after testing to keep the loop going!
  }
}

Your main.dart should now look like this:

// main.dart

import 'dart:io';

class DigitalPet {
  String name;
  int _hunger;
  int _happiness;

  static const int MAX_STAT_VALUE = 10;
  static const int MIN_STAT_VALUE = 0; // Added MIN_STAT_VALUE

  DigitalPet(this.name, this._hunger, this._happiness);

  int get hunger => _hunger;
  int get happiness => _happiness;

  void displayStatus() {
    print('\n--- ${this.name}\'s Status ---');
    print('Hunger: [${_getStatusBar(_hunger)}]');
    print('Happiness: [${_getStatusBar(_happiness)}]');
    print('-------------------------');
  }

  String _getStatusBar(int value) {
    String bar = '';
    for (int i = 0; i < MAX_STAT_VALUE; i++) {
      if (i < value) {
        bar += '#';
      } else {
        bar += '-';
      }
    }
    return bar;
  }
}


void main() {
  print('Welcome to Digital Pet Simulator!');
  stdout.write('What would you like to name your pet? ');
  String? petName = stdin.readLineSync();

  if (petName == null || petName.trim().isEmpty) {
    petName = 'Buddy';
    print('No name entered, calling your pet $petName!');
  } else {
    petName = petName.trim();
    print('Hello, $petName!');
  }

  DigitalPet myPet = DigitalPet(petName!, 5, 5);

  // The main game loop
  while (true) {
    myPet.displayStatus();

    // TEMPORARY: Exit after one loop for testing. We'll remove this soon!
    return;
  }
}

Run it! (dart run main.dart) You'll see your pet's status displayed once, and then the program will exit. This confirms our loop is set up, and the return; is doing its job.


Step 2: User Choices with a switch Statement

Now, let's give the player options for interacting with their pet. We'll display a menu and use a switch statement to handle their choice. A switch statement is excellent when you have a single variable (like the user's choice) that can have several distinct values, and you want to execute different code blocks for each value.

Your Task:

// Inside your while(true) loop, after myPet.displayStatus():
print('\nWhat would you like to do?');
print('1. Feed ${myPet.name}');
print('2. Play with ${myPet.name}');
print('3. Wait (time passes)');
print('4. Exit');

stdout.write('Enter your choice: ');
String? choice = stdin.readLineSync();
// Inside your while(true) loop, after reading 'choice':
switch (choice) {
  case '1':
    print('${myPet.name} is now eating.'); // Placeholder
    break; // Exits the switch statement
  case '2':
    print('${myPet.name} is now playing.'); // Placeholder
    break;
  case '3':
    print('Time passes for ${myPet.name}.'); // Placeholder
    break;
  case '4':
    print('Goodbye! Thanks for playing with ${myPet.name}.');
    return; // Exits the main function, ending the program!
  default: // If none of the above cases match
    print('Invalid choice. Please try again.');
    // No 'break' here; it will loop again and show the menu
}

Your main.dart should now look like this:

// main.dart

import 'dart:io';

class DigitalPet {
  String name;
  int _hunger;
  int _happiness;

  static const int MAX_STAT_VALUE = 10;
  static const int MIN_STAT_VALUE = 0;

  DigitalPet(this.name) // Constructor in Part 3 took _hunger, _happiness. Let's simplify back to just name for default starting
      : _hunger = 5,    // Default initial hunger level
        _happiness = 5; // Default initial happiness level

  int get hunger => _hunger;
  int get happiness => _happiness;

  void displayStatus() {
    print('\n--- ${this.name}\'s Status ---');
    print('Hunger: [${_getStatusBar(_hunger)}]');
    print('Happiness: [${_getStatusBar(_happiness)}]');
    print('-------------------------');
  }

  String _getStatusBar(int value) {
    String bar = '';
    for (int i = 0; i < MAX_STAT_VALUE; i++) {
      if (i < value) {
        bar += '#';
      } else {
        bar += '-';
      }
    }
    return bar;
  }
}


void main() {
  print('Welcome to Digital Pet Simulator!');
  stdout.write('What would you like to name your pet? ');
  String? petName = stdin.readLineSync();

  if (petName == null || petName.trim().isEmpty) {
    petName = 'Buddy';
    print('No name entered, calling your pet $petName!');
  } else {
    petName = petName.trim();
    print('Hello, $petName!');
  }

  // Simplified constructor call, as the class now defaults hunger/happiness
  DigitalPet myPet = DigitalPet(petName!);

  // The main game loop
  while (true) {
    myPet.displayStatus();

    print('\nWhat would you like to do?');
    print('1. Feed ${myPet.name}');
    print('2. Play with ${myPet.name}');
    print('3. Wait (time passes)');
    print('4. Exit');

    stdout.write('Enter your choice: ');
    String? choice = stdin.readLineSync();

    switch (choice) {
      case '1':
        print('${myPet.name} is now eating.');
        break;
      case '2':
        print('${myPet.name} is now playing.');
        break;
      case '3':
        print('Time passes for ${myPet.name}.');
        break;
      case '4':
        print('Goodbye! Thanks for playing with ${myPet.name}.');
        return; // Exit the program
      default:
        print('Invalid choice. Please try again.');
    }
  }
}

Important Note on Constructor: I've slightly adjusted the DigitalPet constructor back to DigitalPet(this.name) and initialized _hunger and _happiness directly in the constructor's initializer list (: _hunger = 5, _happiness = 5;). This simplifies the object creation in main back to DigitalPet myPet = DigitalPet(petName!); and gives your pet default starting stats. If you want to keep passing initial hunger/happiness, you'll need to modify the main function's DigitalPet creation to pass them, e.g., DigitalPet myPet = DigitalPet(petName!, 5, 5);. Both are fine, but the current simplified one makes more sense for a fresh pet.

Run it! (dart run main.dart) Now, your program will display the menu repeatedly. Try typing 1, 2, 3, or 4. Notice how 4 exits the program, and invalid choices just prompt you again.


Step 3: Making Actions Take Time (Asynchronous Programming with async and await)

Currently, when you choose "Feed" or "Play," the message prints instantly. In a real game, these actions would take time! What if we want to simulate this delay without "freezing" our entire program? This is where asynchronous programming comes in. Dart is single-threaded, meaning it generally does one thing at a time. If a long operation (like fetching data from the internet, or a simulated delay) blocked that single thread, your whole program would become unresponsive. Asynchronous programming allows Dart to start a potentially long operation and then go do other work, coming back to the result of that operation when it's ready.

The key players here are:

Your Task:

// Inside your DigitalPet class, after _getStatusBar:
// Method to feed the pet. It's asynchronous!
Future<void> feed() async { // 'async' keyword
  print('${name} is eating...');
  await Future.delayed(Duration(seconds: 2)); // 'await' for a delay

  _hunger = (_hunger - 2).clamp(MIN_STAT_VALUE, MAX_STAT_VALUE); // Decrease hunger
  _happiness = (_happiness + 1).clamp(MIN_STAT_VALUE, MAX_STAT_VALUE); // Increase happiness
  print('${name} finished eating! Hunger decreased, happiness increased.');
}

// Method to play with the pet. Also asynchronous!
Future<void> play() async {
  print('${name} is playing...');
  await Future.delayed(Duration(seconds: 3));

  _happiness = (_happiness + 3).clamp(MIN_STAT_VALUE, MAX_STAT_VALUE); // Increase happiness
  _hunger = (_hunger + 1).clamp(MIN_STAT_VALUE, MAX_STAT_VALUE);     // Increase hunger
  print('${name} finished playing! Happiness increased, hunger increased.');
}

// Method to simulate time passing (pet gets hungrier/sadder)
Future<void> timePasses() async {
  print('Time passes...');
  await Future.delayed(Duration(seconds: 1));
  _hunger = (_hunger + 1).clamp(MIN_STAT_VALUE, MAX_STAT_VALUE);
  _happiness = (_happiness - 1).clamp(MIN_STAT_VALUE, MAX_STAT_VALUE);
  if (_hunger >= MAX_STAT_VALUE) {
    print('${name} is very hungry!');
  }
  if (_happiness <= MIN_STAT_VALUE) {
    print('${name} is very sad!');
  }
}
// Inside your DigitalPet class, after timePasses:
// Check if the pet is in a critical state (e.g., very hungry or sad).
bool needsAttention() {
  return _hunger >= MAX_STAT_VALUE || _happiness <= MIN_STAT_VALUE;
}
// Change main function signature:
void main() async { // Added 'async' here!
  // ...
  // Inside your switch statement, change placeholder prints to actual method calls:
  switch (choice) {
    case '1':
      await myPet.feed(); // Now we await the asynchronous operation
      break;
    case '2':
      await myPet.play();
      break;
    case '3':
      await myPet.timePasses();
      break;
    case '4':
      print('Goodbye! Thanks for playing with ${myPet.name}.');
      return;
    default:
      print('Invalid choice. Please try again.');
  }
}
// Inside your while(true) loop, after the switch statement:
await Future.delayed(Duration(milliseconds: 500));
// Inside your while(true) loop, after the delay:
if (myPet.needsAttention()) {
  print('Warning: ${myPet.name} really needs your attention!');
}

Your main.dart should now contain the complete program, looking exactly like the one you started with in Part 1 (without the comments):

// main.dart

import 'dart:io';

class DigitalPet {
  String name;
  int _hunger;
  int _happiness;

  static const int MAX_STAT_VALUE = 10;
  static const int MIN_STAT_VALUE = 0;

  DigitalPet(this.name)
      : _hunger = 5,
        _happiness = 5;

  int get hunger => _hunger;
  int get happiness => _happiness;

  void displayStatus() {
    print('\n--- ${name}\'s Status ---');
    print('Hunger: [${_getStatusBar(_hunger)}]');
    print('Happiness: [${_getStatusBar(_happiness)}]');
    print('-------------------------');
  }

  String _getStatusBar(int value) {
    String bar = '';
    for (int i = 0; i < MAX_STAT_VALUE; i++) {
      if (i < value) {
        bar += '#';
      } else {
        bar += '-';
      }
    }
    return bar;
  }

  Future<void> feed() async {
    print('${name} is eating...');
    await Future.delayed(Duration(seconds: 2));

    _hunger = (_hunger - 2).clamp(MIN_STAT_VALUE, MAX_STAT_VALUE);
    _happiness = (_happiness + 1).clamp(MIN_STAT_VALUE, MAX_STAT_VALUE);
    print('${name} finished eating! Hunger decreased, happiness increased.');
  }

  Future<void> play() async {
    print('${name} is playing...');
    await Future.delayed(Duration(seconds: 3));

    _happiness = (_happiness + 3).clamp(MIN_STAT_VALUE, MAX_STAT_VALUE);
    _hunger = (_hunger + 1).clamp(MIN_STAT_VALUE, MAX_STAT_VALUE);
    print('${name} finished playing! Happiness increased, hunger increased.');
  }

  Future<void> timePasses() async {
    print('Time passes...');
    await Future.delayed(Duration(seconds: 1));
    _hunger = (_hunger + 1).clamp(MIN_STAT_VALUE, MAX_STAT_VALUE);
    _happiness = (_happiness - 1).clamp(MIN_STAT_VALUE, MAX_STAT_VALUE);
    if (_hunger >= MAX_STAT_VALUE) {
      print('${name} is very hungry!');
    }
    if (_happiness <= MIN_STAT_VALUE) {
      print('${name} is very sad!');
    }
  }

  bool needsAttention() {
    return _hunger >= MAX_STAT_VALUE || _happiness <= MIN_STAT_VALUE;
  }
}

void main() async { // main is now async!
  print('Welcome to Digital Pet Simulator!');
  stdout.write('What would you like to name your pet? ');
  String? petName = stdin.readLineSync();

  if (petName == null || petName.trim().isEmpty) {
    petName = 'Buddy';
    print('No name entered, calling your pet $petName!');
  } else {
    petName = petName.trim();
    print('Hello, $petName!');
  }

  DigitalPet myPet = DigitalPet(petName!);

  while (true) {
    myPet.displayStatus();

    print('\nWhat would you like to do?');
    print('1. Feed ${myPet.name}');
    print('2. Play with ${myPet.name}');
    print('3. Wait (time passes)');
    print('4. Exit');

    stdout.write('Enter your choice: ');
    String? choice = stdin.readLineSync();

    switch (choice) {
      case '1':
        await myPet.feed(); // Awaiting the asynchronous feed method
        break;
      case '2':
        await myPet.play(); // Awaiting play
        break;
      case '3':
        await myPet.timePasses(); // Awaiting timePasses
        break;
      case '4':
        print('Goodbye! Thanks for playing with ${myPet.name}.');
        return;
      default:
        print('Invalid choice. Please try again.');
    }

    await Future.delayed(Duration(milliseconds: 500)); // Small pause

    if (myPet.needsAttention()) {
      print('Warning: ${myPet.name} really needs your attention!');
    }
  }
}

Run it! (dart run main.dart) Now, play with your pet!


Congratulations!

You've completed your guided journey through the essentials of Dart programming by building a fun, interactive application. You started with the finished product, and then rebuilt it from the ground up, learning each concept as you went. Feel free to continue experimenting with this code. Could you add a sleep() method? A vetVisit()? Or even a game over condition if the pet's stats get too extreme? This powerful foundation will serve you well as you explore more advanced Dart features and, especially, if you decide to dive into Flutter for building beautiful mobile, web, and desktop applications!

Back to Digital Pet Overview Previous: Part 3