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:
-
Add
static const int MIN_STAT_VALUE = 0;to yourDigitalPetclass. This provides a lower bound for our pet's stats. Place it right belowMAX_STAT_VALUE.
// Inside your DigitalPet class:
class DigitalPet {
// ...
static const int MAX_STAT_VALUE = 10;
static const int MIN_STAT_VALUE = 0; // New constant!
// ...
}
-
Wrap your main logic in a
while (true)loop : In yourmainfunction, surround themyPet.displayStatus();line and everything that will come after it with awhileloop.
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!
}
}
-
while (true): This creates an infinite loop becausetrueis always true. We'll add a way to break out of this loop later. -
return;: For now, we've added areturn;statement. This immediately exits themainfunction (and thus the program) after one loop, so your terminal doesn't get flooded. We'll remove this once we have a proper exit option.
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:
-
Add menu options and read choice
: Inside your
while (true)loop (and before thereturn;statement), add the menu display and read the user's choice.
// 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();
-
Add the
switchstatement : Still inside thewhile (true)loop, after reading the choice, add the switch block.
// 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
}
-
switch (choice): Theswitchstatement evaluates the value ofchoice. -
case '1':: Ifchoiceis exactly'1', the code under this case executes. -
break;: It's crucial to usebreak;at the end of eachcaseblock (unless you specifically want to fall through to the next case). It tells Dart to exit theswitchstatement. -
return;: In the'4'case, we usereturn;again. This time, it's intentional. It exits themainfunction completely, ending our program. -
default:: This block executes ifchoicedoesn't match any of thecasevalues. -
Remove the temporary
return;: Delete thereturn;line you added previously, just aftermyPet.displayStatus();. This will allow the loop to run continuously until the user chooses option 4.
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:
-
Future: Represents a value or error that will be available at some point in the future. async: Marks a function as asynchronous, meaning it can useawait.-
await: Pauses the execution of anasyncfunction until aFuturecompletes.
Your Task:
-
Add
feed(),play(), andtimePasses()methods toDigitalPet: Place these methods inside yourDigitalPetclass. Pay close attention to theasynckeyword and theFuture<void>return type.
// 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!');
}
}
-
Future<void>: This indicates that the function will complete in the future (Future), and when it does, it won't return any specific value (void). async: This keyword must be present for a function to useawait.-
await Future.delayed(Duration(seconds: X));: This line tells Dart: "Start a delay of X seconds. Don't block the program, but do pause the execution of this specificasyncfunction (feed,play, ortimePasses) until that delay is over. Once it's over, continue from this point." -
.clamp(MIN_STAT_VALUE, MAX_STAT_VALUE): This is a very useful method onint(anddouble) that "clamps" a number between a minimum and maximum value. This prevents our pet's stats from going below 0 or above 10. -
Add
bool needsAttention()method: This is a simple helper to check if our pet is in a critical state.
// 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;
}
-
Update
mainto beasyncandawaitmethod calls : Sincemainwill now callasyncmethods andawaittheir completion,mainitself must also be markedasync.
// 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.');
}
}
-
await myPet.feed();: This tellsmainto pause its execution untilmyPet.feed()(which is anasyncfunction returning aFuture) finishes its delay and stat updates. -
Add a small delay between actions (optional but nice for UX): This gives a slight breather before the menu reappears.
// Inside your while(true) loop, after the switch statement:
await Future.delayed(Duration(milliseconds: 500));
- Add the
needsAttentionwarning:
// 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!
- Choose "1" to feed. Notice the 2-second delay before the stats update.
- Choose "2" to play. Notice the 3-second delay.
-
Choose "3" to let time pass. Notice the 1-second delay and how stats slowly change.
Keep choosing actions and watch how the hunger and happiness bars change.
Try neglecting your pet (keep choosing '3') and see the warning appear!
Finally, choose "4" to gracefully exit the program.
You've successfully built a fully interactive digital pet simulator using variables, control flow (
if,while,switch), functions (methods, getters, private helpers), classes and objects, and fundamental asynchronous programming concepts!
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!