In the last tutorial, Add a Jumping Jack, we realized that having separate text files for the images might be problematic, and we decided to instead include the images within the main program. That's what we'll be doing here.
We could duplicate the "src/d51.rs"/"get_d51()" function for each of the image kinds, but I think we can make it a little easier to draw new images by combining some features of the "get_d51()" function and some features of the "get_image()" function. Let's try it with the Jumping Jack image.
If you have not yet deleted "src/jack.rs", do so now, and then recreate it (or just delete all the contents out of the existing file).
Then copy the following and paste it into "src/jack.rs".
The only differences are that we changed the double-quotes into single-quotes, and then wrapped all of those in one pair of single-quotes, which is on the right-hand side of an assignment statement to a "raw" string (the "r"), assigning the data to a public constant named "JACK" (constants in Rust should be all-caps), and then we finished out that assignment statement with a semi-colon, and added a couple of comments. Very little difference to what the file was before.
So rather than reading this data from a file into a variable, we'll "read" it from this constant, which is sort of like an immutable variable, but a tad less "expensive" in terms of computing resources. In fact, we could use a variable instead if we preferred, but that's slightly more complex to set up in our "src/jack.rs" file, and we'd like to keep this file as simple as possible. The artist who creates an image file will need to use single-quotes on each line rather than double-quotes, and then wrap the whole set in double-quotes to turn it into a single string, like the single string that would be read from a file. In addition, to allow the string to contain single backslashes, we need to set it as a "raw" string; that's the r
just before the first double-quote mark.
Right now, if someone creates a new image, say, a rocket-ship image, etc, that programmer will have to edit the main program in two different places: the "match" section in "extras.rs"/"draw_image()", and the "match" section in "extras"/"parse_opts". To reduce the chance of human error, it would be better if there was only one "match" section that needs to be edited when a new image is added to the program. There are a couple of ways we could accomplish this, but perhaps the best way is to choose, in the "parse_options" function, which image we'll be drawing, and to return the matching image to the calling program ("main()"). After all, which image we draw is an option.
Here are the changes we need to make to the "extras.rs"/parse_opts()" function, commenting out much of the "match" section for now, just to make sure this new method works:
The String::from("JACK"), crate::jack::JACK )
lines could be reduced to String::from("JACK"),
if we added a "use" statement at the top of the "extras.rs" file, like this: crate::jack::JACK )use crate::jack::JACK
(or use crate::jack::*;
, if you wanted to include all the [potential] code within the "jack.rs" file). crate
is roughly equivalent to "the top level of this project we're working on", so it kind of corresponds to "~/projects/RUST/sl/src". Kind of. Not exactly. But close enough for now.
And then in "main()", we'll temporarily comment out the call to "draw_image", and just do a rough drawing from "main() as a test of these changes. We also need to make the program aware of the new "jack" module (roughly, "jack.rs").
This should print out the three frames of Jack, in single-quotes, with a lot of warnings.
$ cargo run -- --kind=jack ... warning: `sl` (bin "sl") generated 10 warnings Finished dev [unoptimized + debuginfo] target(s) in 0.68s Running `target/debug/sl -s=f -k=j` ' \ 0 / ' ' \|/ ' ' | ' ' / \ ' '_/ \_ ' ' ' ' ' ' __0__ ' '/ | \ ' ' / \ ' ' _\ /_ ' ' ' ' ' ' o ' ' /\ /\ ' ' |/ \| ' ' _\ /_ ' ' ' $
Now that the "main()" function has the "image" variable, we'll need to pass that on to the "draw_image()" function. So make the following changes to "main()":
fn main() { let (speed, fly, kind, oops, image) = parse_opts(); // Get the program options from the command-line, if any.println!("{}", image); //draw_image(speed, fly, kind, oops, image ); } // end of main()
We need to make some pretty substantial modifications to both "draw_image()" and "get_image()". The "get_image()" function had been reading the image from a text file, into a &str, and then converting that &str to a vector of vectors of Strings. Now we already have the image in a &str, and just need to convert it to a vector of vector of Strings. So let's rename "get_image()" to "ampstr_to_vecvecstring()", and then modify it as needed:
We could call the above function to convert the &str to a vector of vector of Strings from within the "draw_image()" function, or we could call it before the draw_image() function, from within "main()". It just depends on if we want the "draw_image()" function to take as its input a more plain, more raw text string of the ASCII-art, and process it as needed, or if we want the "draw_image() function to take as its input an already-processed ASCII-art image. The first method is more "integrated"; the second is more UNIX-y, philosophically, in that each piece is small, doing one job and doing it well, and then connecting the pieces together. I think I'll opt for the UNIX-y method; I think it'll be easier to troubleshoot and follow the logic, etc.
So, in "main()", we'll get a &str back from the "parse_opts()" function; then we'll send that off to be converted to a vector of vector of Strings, and then we'll send that vector of vector of Strings to the "draw_image()" function.
Notice we've left the "draw_image()" function commented out. This is so we can simplify things and do some testing at this point:
If you run this, you should get output like this (with quite a few warnings):
Finished dev [unoptimized + debuginfo] target(s) in 0.65s Running `target/debug/sl --kind=jack` "\n' \\ 0 / '\n' \\|/ '\n' | '\n' / \\ '\n'_/ \\_ '\n' '\n\n' '\n' __0__ '\n'/ | \\ '\n' / \\ '\n' _\\ /_ '\n' '\n\n' '\n' o '\n' /\\ /\\ '\n' |/ \\| '\n' _\\ /_ '\n' '\n" Above is the amperstring Below is the converted vecvecstring [[], [" \\ 0 / ", " \\|/ ", " | ", " / \\ ", " _/ \\_ ", " "], [" ", " __0__ ", " / | \\ ", " / \\ ", " _\\ /_ ", " "], [" ", " o ", " /\\ /\\ ", " |/ \\| ", " _\\ /_ ", " "]]
Good! This means our convertor works.
Mostly. I see a problem.
You see that [[], [...
at the beginning of the output after the "Below is the converted vecvecstring" line? That's an empty vector. It's being generated by the \n...
newline in the beginning of the output above the "Above is the amperstring" line.
We don't want that.
That's being generated in our "jack.rs" file, in our "JACK" constant. Here are the first few lines of that constant:
pub const JACK: &str = r" ' \ 0 / ' ' \|/ '
The &str begins right after the r"
, and immediately after that is a newline character (\n
) (which we don't see), followed by what we want to be the first line in the constant, ' \ 0 / '
, followed by another newline character, followed by the next line, ' \|/ '
, followed by another newline character, and so on and so on.
We need to somehow get rid of that first unwanted newline, which is ultimately generating an empty vector. We could rewrite the constant, like so:
pub const JACK: &str = r"' \ 0 / ' ' \|/ '
..., but that's ugly, and puts more of a burden on the artist of the ASCII-art. Other modifications to the constant have similar drawbacks. If we assume that all our image constants are built consistently, in this same way, we can handle the issue in our programming. Let's go that route, and leave the "JACK" constant as we already have it.
We could fix it in the "parse_opts()" function, just before the &str gets returned to be used by the rest of the program. However, it's probably best to let the parse_opts()" function return the constant exactly as it has been written, and let some other part of the program clean it up. Snce the "ampstr_to_vecvecstring()" function is already a custom-designed convertor, it probably makes the most sense to make the clean-up there. Here's that function after necessary (and perhaps unnecessary) changes have been made, including variable-name changes, to perhaps be more self-explanatory:
Now if you try running your test, you should get:
$ cargo run -- --kind=jack ... warning: `sl` (bin "sl") generated 12 warnings Finished dev [unoptimized + debuginfo] target(s) in 0.67s Running `target/debug/sl --kind=jack` "' \\ 0 / '\n' \\|/ '\n' | '\n' / \\ '\n'_/ \\_ '\n' '\n\n' '\n' __0__ '\n'/ | \\ '\n' / \\ '\n' _\\ /_ '\n' '\n\n' '\n' o '\n' /\\ /\\ '\n' |/ \\| '\n' _\\ /_ '\n' '\n" Above is the amperstring Below is the converted vecvecstring [["' \\ 0 / '", "' \\|/ '", "' | '", "' / \\ '", "'_/ \\_ '", "' '"], ["' '", "' __0__ '", "'/ | \\ '", "' / \\ '", "' _\\ /_ '", "' '"], ["' '", "' o '", "' /\\ /\\ '", "' |/ \\| '", "' _\\ /_ '", "' '"]]
Yay! No more unwanted newline or empty vector!
Don't let the double-backslashes throw you; they're only used internally in the innards of Rust, and the "debug formatter" (the "{:?}"
) doesn't know to dispay them as a single backslash. But the default println! formatter does. You can see this if we print the variable a little better in our test:
That should generate:
warning: `sl` (bin "sl") generated 12 warnings Finished dev [unoptimized + debuginfo] target(s) in 0.69s Running `target/debug/sl --kind=jack` \ 0 / \|/ | / \ _/ \_ __0__ / | \ / \ _\ /_ o /\ /\ |/ \| _\ /_
Now let's get our "draw_image()" function to work. Notice how we have gotten rid of the entire second "match" section from our program. Notice that I've changed some variable names to make them more self-explanatory.
And then we restore our "main()" back to normal"
And that should result in a working Jumping Jack.
I notice though, in Fly mode, Jack flies off the top of my terminal window long before he gets to the edge, so I think I'll configure the images to start over at the bottom if they fly completely off the top. In "draw_image()":
... if inner_vec < 0 { // If we've gotten down to last inner_vec... inner_vec = count_of_inner_vecs - 1; // ... start over. if fly { // And check to see if we're flying. If so... line = -1 + row + height; // ... erase last (bottom) line drwn with enough repeated spaces fo train's length. my_mvaddstr( line, col, &" ".repeat(length as usize), screen_width, screen_height, ); // No need to refresh(); next inner_vec will do it for us. row -= 1; // ... raise the next drawing one row up.if row < 0 - height { row = screen_height; } } } ...