Launch
Mission control: the program runs a pre-flight checklist, asks how long a countdown you want, counts down with a pause between the numbers, and launches a rocket up the terminal. It is the most playful project in the course, and also a careful one β the answer to its one question is handled in every way it can arrive.
Every part of it has appeared before. The checklist is an array of text, the countdown is a loop with a pause from the Time package, and the question is the line reading from Input, checked the way Parse checks a number. It is a checkpoint for Part 20: Utilities.
How it is put together
Main runs four stages in order, and only the question can end the program early:
flowchart LR
check["Checklist<br/>array + Pause"] --> ask["Ask: count down from?<br/>1 to 10, Enter for 10"]
ask -- "end of input" --> scrub(["Mission scrubbed<br/>status 0"])
ask -- "read error" --> radio(["Radio down<br/>status 1"])
ask -- "a valid answer" --> count["T minus β¦<br/>while count > 0"]
count --> lift["Ignition and<br/>rocket frames"]| Piece | Lessons it uses |
|---|---|
| Checklist and rocket frames | Array, For, Character (\u{β¦}) |
Pause | Duration, Discard |
| The question | Input, String view, Guard |
| Checking the answer | Parse, Catch fallback |
| The countdown | While, Assignment |
A pause that may be skipped
SleepFor returns a fallible: the operating system may refuse to sleep. A countdown that skips a pause is only quicker, never wrong, so Pause discards a failure β on purpose, and visibly:
func Pause(milliseconds: int64) {
SleepFor(Duration::FromMilliseconds(milliseconds)) catch { else => {} };
}
catch { else => {} } is the standard way to say "I have considered this failure and it does not matter". Writing the call without it is an error, as Common mistakes shows.
A question with a guaranteed answer
The question is asked in a loop that runs until count holds a usable number. count starts at 0, which is not a valid answer, so the loop runs at least once:
let answer = builder.View().Trim();
if answer.IsEmpty() {
count = 10;
} else {
// Text that is not a number becomes 0, which the range check refuses as well.
let asked = ParseInt32(answer) catch { else => 0 };
if asked >= 1 && asked <= 10 {
count = asked;
} else {
PrintLine("Mission rules allow 1 to 10. Say again?");
}
}
Three kinds of answer are handled with very little code:
| Typed | What happens |
|---|---|
| Nothing | The default, 10 |
| 1 to 10 | That number |
| Anything else | Asked again β text parses to the fallback 0, which is out of range |
Folding "not a number" into "out of range" with catch { else => 0 } is a neat trick here, because 0 is never a valid answer. Whatever happens, the countdown is bounded: it can never be asked to count from a million.
Before all that, the same guarded match as in Guess decides what the end of the input means. Here nobody gave the go, so the mission is scrubbed β a normal outcome with status 0. A read error is a broken radio, and exits with status 1.
Countdown and liftoff
The countdown is a plain while that pauses between numbers:
while count > 0 {
PrintLine(" T minus {}...", count);
Pause(400);
count -= 1;
}
The rocket climbs because each frame is printed on a new line with a little less indentation than the one before, and the pauses between them let you watch it go. The emoji are written as \u{1F680} escapes in the source, so the file itself stays plain ASCII and every editor shows it the same way.
The program
The whole lesson is one package in the Examples repository. Its comments explain every step.
// Mission control: run the checklist, ask how long a countdown you want, count down, and launch.
//
// Every part of this has appeared before. The checklist is an array of text, the countdown is a
// loop with a pause from the Time package between steps, and the question is the line reading
// from the Input lesson, with the number checked the way the Parse lesson checks it.
//
// What makes it a whole program is that the input is handled in every way it can arrive. A
// number from 1 to 10 sets the countdown and an empty line takes the default of 10, so the
// countdown is always bounded. Anything else is asked again. And if the input ends before an
// answer arrives (Ctrl+Z and Enter on Windows, Ctrl+D elsewhere, or the end of piped input),
// nobody gave the go, so the mission is scrubbed.
import Allocator::{ Allocator, SystemAllocator };
import Format::ParseInt32;
import Io::{ IoErrorKind, Print, PrintLine, ReadLine };
import Text::StringBuilder;
import Time::{ Duration, SleepFor };
// A pause that does not happen only makes the countdown quicker, so a failed sleep is ignored
// on purpose rather than by accident.
func Pause(milliseconds: int64) {
SleepFor(Duration::FromMilliseconds(milliseconds)) catch { else => {} };
}
func Main() -> int {
PrintLine("\u{1F680} RUX MISSION CONTROL \u{1F680}");
PrintLine("========================");
let checklist = [
"fuel ............. loaded",
"crew ............. strapped in",
"weather .......... clear skies",
"snacks ........... dangerously low",
"borrow checker ... satisfied"
];
for item in checklist {
PrintLine(" {} \u{2705}", item);
Pause(150);
}
PrintLine();
var system = SystemAllocator();
let allocator: Allocator = system;
var builder = StringBuilder(allocator);
var count = 0;
while count == 0 {
Print("Count down from? (1 to 10, Enter for 10) ");
builder.Clear();
match ReadLine(builder) {
.Success(_) => {},
.Failure(error) if error.kind == IoErrorKind::EndOfStream => {
PrintLine();
PrintLine("No go from the flight director. Mission scrubbed. \u{1F6D1}");
return 0;
},
.Failure(_) => {
PrintLine("The radio is down: the input could not be read.");
return 1;
}
}
let answer = builder.View().Trim();
if answer.IsEmpty() {
count = 10;
} else {
// Text that is not a number becomes 0, which the range check refuses as well.
let asked = ParseInt32(answer) catch { else => 0 };
if asked >= 1 && asked <= 10 {
count = asked;
} else {
PrintLine("Mission rules allow 1 to 10. Say again?");
}
}
}
PrintLine();
while count > 0 {
PrintLine(" T minus {}...", count);
Pause(400);
count -= 1;
}
PrintLine(" \u{1F525} IGNITION \u{1F525}");
Pause(500);
// Each frame is one line, so the rocket climbs up the terminal as it prints.
let frames = [
" \u{1F680}",
" \u{1F680}",
" \u{1F680}",
" \u{1F680}",
" \u{1F680}",
" \u{1F680}",
" \u{1F680}",
" \u{1F680}"
];
for frame in frames {
PrintLine(frame);
Pause(120);
}
PrintLine();
PrintLine(" \u{2B50} \u{1F30D} \u{2B50}");
PrintLine(" Liftoff! The crew waves from the window. \u{1F44B}");
return 0;
}
Besides Io, its Rux.toml lists Allocator, Format, Text and Time under [Dependencies].
Run it
cd Examples/Projects/Launch
rux run
π RUX MISSION CONTROL π
========================
fuel ............. loaded β
crew ............. strapped in β
weather .......... clear skies β
snacks ........... dangerously low β
borrow checker ... satisfied β
Count down from? (1 to 10, Enter for 10) 20
Mission rules allow 1 to 10. Say again?
Count down from? (1 to 10, Enter for 10) 3
T minus 3...
T minus 2...
T minus 1...
π₯ IGNITION π₯
π
π
π
π
π
π
π
π
β π β
Liftoff! The crew waves from the window. π
The answer can be piped in. With no input at all, nobody gave the go, and after the checklist the run ends with:
"3" | rux run
$null | rux run
Count down from? (1 to 10, Enter for 10)
No go from the flight director. Mission scrubbed. π
The program waits for an answer after the checklist. Run it, type a number from 1 to 10 (or just press Enter for 10) and watch the countdown. End the input instead β Ctrl+Z and Enter on Windows, Ctrl+D elsewhere β to scrub the mission. The piped examples above are written for PowerShell; in a POSIX shell, echo 3 | rux run and rux run < /dev/null do the same. Whether the emoji show up depends on your terminal and its font.
Common mistakes
SleepFor and ignoring the result.SleepFor(Duration::FromMilliseconds(milliseconds)); on its own is refused: error: fallible result of type '! TimeError' is discarded. Even a failure you do not care about has to be discarded on purpose, with catch { else => {} }.Accept any number that parses and someone will type
1000000. At 0.4 seconds a step, the program would still be counting four and a half days later. Checking the range on input keeps every later part of the program simple.Try it yourself
- Add a "hold" option: if the answer is
h, print "Holding." and ask again. - Let the checklist fail: make one item
"weather .......... storm"and scrub the launch if any item does not end with a known good word. (Hint: String view can test how text ends.) - Time the whole countdown with a Stopwatch and print how long it really took, pauses and all.
- Make the rocket climb diagonally: change both the indentation and the number of empty lines between frames.