Assignment 01: RPG Character Creator

Variables, memory, control structures, and functions, in C++

Lab opening · Week 00 full screen
Note

Your own private GitHub repository is waiting for you, but get the order right: start your Coder workspace first, connect VS Code to it, and only then clone the repository onto the Calvin machine (see Getting the code below). Cloning onto your own laptop instead gives you code you cannot compile.

Plan to set aside about 2 to 2.5 hours. The first part (character sheet) is designed to be started in lab with your class; the adventure system and function decomposition are the take-home extension. The pointer section and the while-loop refactor are the two places most students slow down, give yourself extra time there.

Objectives

By the end of this assignment, you will be able to:

  • Declare variables of types int, unsigned, double, string, char, bool, long, and const unsigned
  • Use the address-of operator & to print where variables live in memory
  • Declare a pointer, assign it an address, and dereference it with *
  • Print a value in decimal, octal, and hexadecimal using stream manipulators
  • Explain what happens when an unsigned integer decrements below zero
  • Use if / else if / else to branch on a numeric range
  • Write a while loop that validates user input
  • Use a switch statement with fall-through cases
  • Write for loops that iterate both ascending and descending over arrays
  • Decompose repeated logic into named functions that take parameters and return values

These objectives map to course skills A1, A2, A3, and A4.


1. Introduction

You are writing the character-creation engine for a fantasy RPG. Every character in the game has a set of stats, and those stats live somewhere in the computer’s memory. Your job is to declare those stats, inspect their locations, and build the systems that govern how a character is described, how they join a guild, and how they accumulate experience points.

Unlike Python, where you just write health = 100 and move on, C++ requires you to say exactly what type of value health is, and that choice has real consequences. Is it a number that can go negative? An integer or a decimal? Can it be changed, or is it a constant? These decisions affect how much memory is used, what operations are allowed, and what happens at the edge cases. The RPG bugs you encounter in this assignment are exactly the bugs that show up in real games written in C++.

Here is a side-by-side comparison of how Python and C++ handle variable declarations:

Concept Python C++
Integer variable health = 100 int health = 100;
Unsigned integer (no direct equivalent) unsigned health = 100;
String name = "Aria" string name = "Aria";
Character code = 'A' char code = 'A';
Decimal number agility = 3.14 double agility = 3.14;
Boolean is_alive = True bool isAlive = true;
Constant MAX_HEALTH = 1000000 const unsigned MAX_HEALTH = 1000000;
Address of variable (not accessible) &health
Pointer (not accessible) unsigned* ptr = &health;

Two things to notice: C++ needs a semicolon at the end of every statement, and true/false are lowercase. The rest is fairly readable once you know the pattern.


2. Getting the Code

Your private GitHub repository for this assignment is created for you inside the course organization, with the starter code already in it. There is nothing to accept and nothing to set up, if you are on the roster, it is there.

Clone it, replacing YOURUSERNAME with your GitHub username. If gh asks you to sign in, see step 9 of Getting Started:

gh repo clone 26fa-cs112/cs112-a01-YOURUSERNAME
cd cs112-a01-YOURUSERNAME
Note

Cannot find it? Ask GitHub what you have.

If gh repo clone answers Could not resolve to a Repository, this lists every repository you can actually see, spelled exactly right:

gh repo list 26fa-cs112

If yours is in that list, you mistyped the name, copy it from the output and try again. Watch for usernames that already contain a hyphen or a number; the pattern is cs112-<assignment>-<your username>, so the two halves run together and it often looks wrong when it is right.

If the list is empty, or the command errors, that is mine to fix rather than yours to work around. The usual causes are an organization invitation you have not accepted yet (check your email, including spam and the Promotions tab), or a username I mistyped when I built the roster. Either way, email me at eric.araujo@calvin.edu and I will sort it out the same day.

While you wait, do not create a repository yourself and do not start the work somewhere else. Only the repository I create for you is connected to grading, so anything written outside it has to be moved by hand later, and that is a worse afternoon than a short wait.

Open the folder in VS Code and you are ready to start.


3. The File You Will Edit

Open functions.cpp in VS Code. This is the only file you need to edit. functions.h and main.cpp are already complete, read through them to understand the structure, but do not change them.

Note

Two lines at the top of functions.h that need explaining.

Open it and you will see:

#ifndef FUNCTIONS_H
#define FUNCTIONS_H

with #endif at the very bottom. That is an include guard, and every header you meet this semester has one.

Here is the problem it solves. #include is a literal paste: the compiler drops the file’s whole text into whatever included it. Headers include other headers, so the same file often gets pasted into one compilation two or three times without anyone meaning to, and a second copy of the same declarations is an error, not a harmless repeat.

The guard makes the second paste do nothing. The first time through, FUNCTIONS_H is not defined, so the compiler defines it and reads the file. Any later time, it is already defined, so everything up to #endif is skipped.

You will write your own from a04 onward, when you start creating classes. The convention is the file’s name in capitals with the dot turned into an underscore, so Queue.h gets QUEUE_H.

Note

And one line at the top of main.cpp.

In a00 it was simply int main(). Here it is:

int main(int argc, char* argv[]) {

Those two parameters are how a program reads what you typed after its name on the command line, which is what makes ./a01 step1 possible at all.

  • argc, argument count. How many words were on the line, including the program’s own name. Typing ./a01 step1 gives argc == 2.
  • argv, argument vector. The words themselves, as an array. argv[0] is "./a01" and argv[1] is "step1".

That is why main.cpp checks argc < 2 before doing anything: if you run ./a01 with no step, there is no argv[1] to read, and reading it anyway would be your first taste of undefined behavior. It prints the usage line instead.

char* means pointer to a character, and char* argv[] an array of them. Pointers are exactly what TODO 1 is about. You are not expected to be comfortable with that line yet; you are expected to recognize it when you meet it again.

To compile and run a specific step:

make
./a01 step1

To run all automated tests:

make test

Run make test after finishing each TODO. The tests give PASS/FAIL for each check, with hints when something is wrong. Work through failures one at a time.


4. Part 1: The Character Sheet

Important

Where every TODO on this page goes.

All of your work for Parts 1, 2 and 3 happens in one file: functions.cpp. You will not create any new files, and you will not edit functions.h, main.cpp, makefile or test.py.

Open functions.cpp and scroll through it once before you start. Every TODO on this page has a matching comment block inside it, in the same order and with the same number, and each block ends with a marker:

void character_sheet() {

    // ---------------------------------------------------------------------
    // TODO 1a, Declare character variables
    // ---------------------------------------------------------------------
    // ... the instructions, repeated from this page ...

    // YOUR CODE HERE

Write your code where that // YOUR CODE HERE marker is, inside the function whose name the section mentions. Leave the comment blocks alone: they are your instructions, and deleting them costs you nothing but makes the file much harder to work in.

So “In character_sheet(), declare the following seven variables” means: find void character_sheet() in functions.cpp, find the TODO 1a block inside it, and type your seven declarations under its // YOUR CODE HERE.

Tip: in VS Code, Ctrl+F (Cmd+F on a Mac) and typing TODO 1a jumps you straight there.

Tip

Type the code. Do not paste it.

Getting Started made this point about terminal commands, and it matters at least as much here. Every C++ example below has a copy button, and using it will get you a working functions.cpp while leaving you unable to write a line of C++ from a blank file, which is what the check-in exams ask for.

Typing is also how the syntax stops being decoration. Type unsigned* hpPtr = &health; yourself and you have to decide where the * goes and which side the & is on; paste it and you have decided nothing. The compiler errors you cause while typing are the cheapest teaching in the course, and they are much easier to fix in the two lines you just wrote than in fifty lines you pasted.

Read the example, look away from it, then type it. If you cannot, you have found exactly the thing you do not understand yet, which is useful to know now rather than in week eight.

TODO 1a, Declare character variables

In character_sheet(), declare the following seven variables. The types and names must match exactly, the autograder checks them.

Variable Type Initial value
MAX_HEALTH const unsigned 1000000
name string Your first name (in double quotes)
classCode char First letter of your last name (in single quotes)
agility double 3.1415
isAlive bool true
gold long 0
health unsigned 100
Tip

Single quotes vs. double quotes. In C++, 'A' is a single character (char). "A" is a string, even if it contains only one letter. Use single quotes for char variables and double quotes for string variables.

Warning

Unused variable warnings. After TODO 1a, compiling will produce warnings about unused variables. This is expected, you will use them in TODO 1b. Warnings are not errors; the program still compiles and runs.


TODO 1b, Print memory addresses

Every variable you declare occupies space somewhere in RAM. In C++, the address-of operator & gives you the memory address of any variable:

cout << &health;       // prints something like: 0x7ffd3a1b2c08

For char variables, cout << &classCode would treat the address as a C-style string and print garbage. Use a cast to (void*) to force it to print the address instead:

cout << (void*)&classCode;

Print each variable’s address on its own line using these exact labels (the autograder checks for them verbatim):

"MAX_HEALTH is at: "
"name is at: "
"classCode is at: "
"agility is at: "
"isAlive is at: "
"gold is at: "
"health is at: "

Your output will look something like this (the actual numbers will differ):

MAX_HEALTH is at: 0x7ffd3a1b2c10
name is at: 0x7ffd3a1b2bf0
classCode is at: 0x7ffd3a1b2bef
agility is at: 0x7ffd3a1b2be0
isAlive is at: 0x7ffd3a1b2bde
gold is at: 0x7ffd3a1b2bd0
health is at: 0x7ffd3a1b2bc8
Tip

Why are the addresses different on every run? Modern operating systems use address-space layout randomization (ASLR), they deliberately place your program at a random memory location each time it runs. This is a security feature that makes it harder to exploit memory bugs.


TODO 1c, Declare a pointer and use it

A pointer is a variable that holds a memory address. Where a regular variable holds a value (like 100), a pointer holds the address where a value lives (like 0x7ffd3a1b2bc8).

// Declare a pointer to health:
unsigned* hpPtr = &health;

// Print the address the pointer holds (same as &health):
cout << "hpPtr points to: " << hpPtr << endl;

// Dereference: follow the pointer to get the value at that address:
cout << "Value at hpPtr: " << *hpPtr << endl;

In Python, every variable is already a reference, you can never see or manipulate the raw address. In C++, you can, which gives you both power and responsibility.

Use these exact labels:

"hpPtr points to: "
"Value at hpPtr: "

Expected output (address varies):

hpPtr points to: 0x7ffd3a1b2bc8
Value at hpPtr: 100

Test with ./a01 step1, then make test.


Checkpoint: save TODO 1 to GitHub

TODO 1 is the whole of character_sheet(), and it is a natural place to stop and save. Right now your work exists in one place only: a folder on the Coder server. Push it and there are two copies, one of them somewhere you cannot break.

In VS Code, click the Source Control icon in the left sidebar, it looks like a branch, and it will have a small badge showing how many files you have changed.

  1. Your changed files are listed under Changes. Hover over functions.cpp and click the + to stage it.
  2. Type a short message in the box at the top, such as TODO 1 complete.
  3. Click ✓ Commit.
  4. Click Sync Changes to send it to GitHub.
Tip

The same thing in the terminal.

git add functions.cpp     # the + button: stage this file
git commit -m "TODO 1 complete"    # the ✓ button
git push                  # Sync Changes

The buttons and the commands do exactly the same thing. Use whichever you prefer.

Note

Expect a red X on GitHub, and do not let it worry you.

Pushing starts the automatic check. TODO 2 through TODO 10 are still empty, so the check fails and GitHub shows a red X rather than a green tick. That is the correct result for unfinished work.

The red X does not mean zero. The check ran every test and recorded what passed, so the marks you have already earned are stored. The tick is not the grade; it only turns green when every single test passes, which for this assignment means all ten TODOs.

Do not wait until the assignment is finished to push, and do not push only once. Commit and push whenever something works, at the end of each TODO if you like. It costs ten seconds, it means a broken laptop or a lost workspace costs you nothing, and later in the semester it gives you a working version to go back to when an experiment goes wrong.


TODO 2, Number systems

In show_code(), create an int called code initialized to 37, then print it in all three number systems:

cout << "Decimal:     " << dec << code << endl;
cout << "Octal:       " << oct << code << endl;
cout << "Hexadecimal: " << hex << code << endl;

The stream manipulators dec, oct, and hex change how subsequent integers are formatted in the output stream. They stay in effect until you change them.

Expected output:

Decimal:     37
Octal:       45
Hexadecimal: 25
Tip

37 in binary is 100101. Grouping into three bits from the right gives 100 101, which is 4 and 5 in decimal, so octal is 45. Grouping into four bits gives 0010 0101, which is 2 and 5, so hex is 25.

Test with ./a01 step2, then make test.


TODO 3, Arrays and sizeof

In inventory_size(), declare an array of 20 floats:

float inventory[20];

Then print its total size in bytes using sizeof:

cout << "Inventory size in bytes: " << sizeof(inventory) << endl;

sizeof(inventory) returns the total size of the whole array: 20 floats × 4 bytes each = 80 bytes. Compare this to sizeof(float), which only gives you the size of one float.

Expected output:

Inventory size in bytes: 80

Test with ./a01 step3, then make test.


TODO 4, Unsigned integer underflow

In health_underflow(), you will discover one of the most infamous bugs in game programming. Declare an unsigned integer set to 0, then decrement it:

unsigned int hp = 0;
hp--;
cout << "HP after underflow: " << hp << endl;

You will not see -1. Add a comment explaining why.

Warning

What is an unsigned integer? A regular int can hold negative values (roughly −2 billion to +2 billion). An unsigned int can only hold non-negative values (0 to roughly 4 billion). Decrementing below zero does not produce a negative number, it wraps around to the maximum value. This is called integer underflow.

This exact bug has caused real problems in shipped games: a character’s health drops from 1 to 0, and instead of dying they suddenly have 4,294,967,295 HP.

Expected output:

HP after underflow: 4294967295

Test with ./a01 step4, then make test.


5. Part 2: The Adventure System

Same file, same pattern: each TODO below has a numbered comment block in functions.cpp with a // YOUR CODE HERE marker inside it.

TODO 5, Power tier rating (if / else if / else)

In rate_power(), read an unsigned power level from the user and assign a letter tier using if / else if / else:

Power Tier
90 or above A
80 – 89 B
70 – 79 C
60 – 69 D
Below 60 F

Use these exact prompt strings:

"Enter your power level (0-100): "
"Your power tier is: "

In Python this would be a chain of elif. In C++:

if      (power >= 90) tier = 'A';
else if (power >= 80) tier = 'B';
else if ...
Tip

Declare tier as a char before the if chain, then assign to it inside each branch. Print it after the chain ends.

Test with ./a01 step5, then make test. The autograder checks every boundary value (89, 90, 79, 80, etc.), so be precise with your >= conditions.


TODO 6, Guild membership cost (while + if / else)

In guild_cost(), you will combine two new control structures: a while loop for input validation and a nested if / else for cost lookup.

Step A, Validate the guild name with a while loop:

string guild;
cout << "Enter guild name: ";
cin >> guild;

while (guild != "silver" && guild != "gold") {
    cout << "I'm sorry, \"" << guild << "\" is not a known guild." << endl;
    cout << "Enter guild name: ";
    cin >> guild;
}

The \" inside a string literal prints a literal double-quote character. The loop keeps running as long as the guild name is neither "silver" nor "gold".

Step B, Read the duration:

unsigned months;
cout << "Enter number of months: ";
cin >> months;

Step C, Compute the cost using this table:

Guild 1 month 2–6 months 7+ months
silver $90 $60 $60
gold $90 $70 $35

Step D, Print the result. Use this exact string, immediately followed by the cost and then a period, with no spaces between the three:

"The cost for your guild is $"

Example session:

Enter guild name: bronze
I'm sorry, "bronze" is not a known guild.
Enter guild name: gold
Enter number of months: 3
The cost for your guild is $70.

Test with ./a01 step6, then make test. The autograder tests every cost cell in the table, both boundary values (months 6 and 7), and multiple invalid inputs.


TODO 7, Class affinity (switch)

In class_type(), read a single lowercase character from the user and use a switch statement to determine whether it is a vowel (magical class) or consonant (physical class).

The key technique here is fall-through: multiple case labels can share the same body by stacking them without a break:

switch (letter) {
    case 'a':
    case 'e':
    case 'i':
    case 'o':
    case 'u':
        cout << "Vowel-class: magical affinity." << endl;
        break;
    default:
        cout << "Consonant-class: physical affinity." << endl;
}

Use these exact prompt strings:

"Enter your class letter: "
"Vowel-class: magical affinity."
"Consonant-class: physical affinity."
Note

y is a consonant for this exercise. The autograder checks it.

Test with ./a01 step7, then make test.


TODO 8, XP thresholds (for loop, ascending)

In xp_levels(), create an array of 8 unsigned values and fill it with multiples of 7 using a for loop:

unsigned xp[8];
for (int i = 0; i < 8; i++) {
    xp[i] = i * 7;   // 0, 7, 14, 21, 28, 35, 42, 49
}

Then verify three specific values using assert:

assert(xp[0] == 0);
assert(xp[3] == 21);
assert(xp[7] == 49);

If an assertion fails, the program crashes immediately with an error, that means your loop is filling the array incorrectly. Fix the loop and rerun.

If all three pass, print:

All XP thresholds verified!

Test with ./a01 step8, then make test.


TODO 9, Gold sum (for loop, descending)

In gold_sum(), create an array of 100 doubles, fill it forward, then sum it in reverse:

double drops[100];
for (int i = 0; i < 100; i++) {
    drops[i] = i * 0.1;   // 0.0, 0.1, 0.2, ..., 9.9
}

double total = 0.0;
for (int i = 99; i >= 0; i--) {   // ← descend from 99 down to 0
    total += drops[i];
}

cout << "Total gold collected: " << total << endl;
Warning

Why int, not unsigned, for the loop variable? A descending for loop eventually reaches i = -1 as its stopping condition. An unsigned variable cannot represent -1, it wraps to 4,294,967,295 (the same underflow you saw in TODO 4). The loop would run forever. Always use int when your loop variable needs to go below zero.

Expected output:

Total gold collected: 495

Test with ./a01 step9, then make test.


6. Part 3: Function Decomposition

In TODO 6 you wrote guild_cost() as one big function. It does three distinct things: validates the guild name, reads the duration, and computes the cost. A function that does three things is harder to test and reuse than three functions that each do one thing.

In this part you will refactor that logic into three smaller functions. play(), already written in the starter code, calls them in sequence.

These three are at the bottom of functions.cpp, in the same // YOUR CODE HERE pattern as everything above. One difference: each already ends with a placeholder line such as

    return ""; // replace this line

which exists only so the starter compiles. Delete it once your function returns a real value. After your three functions work, play() should produce the same output as guild_cost() without any changes on your part.

TODO 10a, getGuild()

Extract the while-loop validation from TODO 6. The function should:

  • Print "Enter guild name: " and read input
  • If invalid, print the rejection message and ask again
  • Return the valid guild name as a string
string getGuild() {
    string guild;
    // your while loop here
    return guild;
}

TODO 10b, getDuration()

Extract the months-input from TODO 6. The function should:

  • Print "Enter number of months: " and read an unsigned
  • Return the value
unsigned getDuration() {
    unsigned months;
    // your cin here
    return months;
}

TODO 10c, computeCost()

Extract the cost-table logic from TODO 6. This function:

  • Takes guild (a string) and months (an unsigned) as parameters
  • Returns the cost as an unsigned
  • Contains no cout or cin, pure computation only
unsigned computeCost(string guild, unsigned months) {
    // your if/else table here
    return cost;
}
Tip

In, out, and in/out parameters. computeCost takes two in parameters, that is, values passed in for the function to read, and returns one value. There is no need for cin or cout inside it. If you find yourself reaching for cin inside computeCost, step back: the caller (play) is responsible for gathering input; computeCost is only responsible for the math.

The pre-written play() function

Open functions.cpp and find play() near the bottom. It is already complete:

void play() {
    string   guild  = getGuild();
    unsigned months = getDuration();
    unsigned cost   = computeCost(guild, months);
    cout << "The cost for your guild is $" << cost << "." << endl;
}

Study this function. Anyone reading it immediately understands what it does, not because it has comments, but because the function names say it all. This is what well-decomposed code looks like.


7. Testing Your Work

After completing each TODO, run:

make test

You will see a PASS or FAIL for each of the 89 test cases. Read the hints on any FAILs, they tell you exactly which prompt string is missing, which boundary value is wrong, or which loop is behaving unexpectedly.

Tip

If you get a build error, the hints do not matter yet, fix the compiler error first. The error message tells you the file name and line number. Read it carefully.

When all tests pass:

Results: 89/89 passed
All tests passed! Your character sheet is complete.
Note

There are no hidden tests. What you run with make test is exactly what the grader runs, all 89 checks. If it passes here, it passes there.

Grading always runs the official test suite, not the copy in your repository. Editing test.py will not change your score.


8. Submit

Important

Before pushing, open README.md and add your name.

In VS Code, click the Source Control icon in the left sidebar. You will see your changed files. Type a commit message (e.g., "Complete a01"), click the checkmark to commit, then sync to push to GitHub. From the terminal, that is:

git add .
git commit -m "Complete a01"
git push

Every push is checked automatically. Within a minute or two the Actions tab of your repository shows a run named Autograde. Open it: the job summary says how many checks passed, and names any that failed. Push as many times as you like before the deadline, the most recent submission is the one that counts.


9. When I Push a Fix Into Your Repository

Assignments occasionally need correcting after they have gone out. A test turns out to be checking the wrong thing, a comment in functions.h says something misleading, a makefile flag is wrong. When that happens I do not ask twenty-five people to re-clone. I push the corrected file straight into each of your repositories, and I tell the class that I have done it.

Only files you do not own move that way, and each of them says so at the top:

NOTE TO STUDENTS: You do NOT need to edit this file.

For this assignment that is functions.h, main.cpp, makefile and test.py. functions.cpp is yours, and nothing I run will touch it.

What that means for you

Your repository now has a commit on GitHub that your Coder copy does not. Until you fetch it, you are working against the old file, and make test on your machine may disagree with the Autograde result on GitHub, which is exactly the confusion the fix was meant to end.

So: pull before you start working, every session, for the rest of the semester. It takes a second and it costs nothing when there is nothing to collect.

In VS Code, the Source Control panel is where this lives. The Sync Changes button carries a small ↓ count when GitHub has commits you do not: clicking it brings them down and sends yours up in one go. If you would rather only collect, use the menu at the top of the panel and choose Pull.

Tip

The same thing in the terminal.

git pull

pull is two operations with one name: fetch what is on GitHub, then join it to what you have.

Important

Commit your own work before you pull. Git will refuse to pull on top of uncommitted edits to a file it needs to replace, and the refusal is the polite outcome. Commit first and your work is recorded, recoverable, and impossible to lose in the process.

Then pull, and let it merge. Once I have pushed, there is no way to send your work up until you have brought mine down. git push on its own will be rejected with “the remote contains work that you do not have locally”, and that is not an error you fix by trying harder: it is Git telling you to pull first. Pull, which merges the two lines of history, and then push.

Tip

If git pull answers this:

fatal: Need to specify how to reconcile divergent branches.

it is asking how you want the two lines joined, and refusing to guess. This course always merges. Set that once and the message never comes back:

git config --global pull.rebase false

Then git pull as usual. You did this in Getting Started; this is the same setting, for a new machine or a session where you skipped it.

Why this is called merging

You now have two lines of history that both grew from the same starting point. Yours is the branch called main in your Coder folder, where you committed TODO 4. Mine is origin/main, the copy on GitHub, where I committed the fix. Neither is a continuation of the other; they forked.

git pull fetches my line and merges the two, which means it writes one new commit joining both. That commit is the only one in your repository with two parents, and if you ever look at the history graph it is the point where the two strands come back together. It is a normal, healthy thing for a repository to contain, not a sign that something went wrong.

If an editor opens asking you to confirm a merge message, the default text is fine. In VS Code, press Ctrl+S and close the tab. If you land in a terminal editor instead, nano exits with Ctrl+O, Enter, Ctrl+X, and vim exits with :wq then Enter.

If it reports a conflict

A conflict means we both changed the same lines of the same file. Given the four files above are the only ones I push, that can only happen if you edited a file whose header told you not to, so it should be rare. When it does happen, my version is the one you want: it is the file the grader will use regardless of what your copy says.

git checkout --theirs functions.h
git add functions.h
git commit
Note

--theirs reads backwards here, and it trips up everyone the first time. During a merge, “ours” means the branch you were sitting on and “theirs” means the one arriving. The arriving one is mine, so --theirs is my file.

To back out of the merge entirely and return to exactly where you were, run git merge --abort. Nothing is lost, and you can email me and we will sort it out together. Do not delete the folder and re-clone: that throws away any work you have not pushed.