4 Hugues Ross Writes a Devlog: 01/01/2016 - 02/01/2016
Hugues Ross

1/29/16

Capstone Update 11: Bugs in the wiring

Like last week's post, this Capstone post is going to be a bit short. Because of my schedule, I haven't been able to do much Capstone work outside of weekends. In addition, I didn't really have much to do until last Wednesday. As a result, I don't really have much to share beyond some truly riveting refactoring work. Because of the dearth of work, I'm going to be talking about a bug I found last week while testing a shader.

We have many interactive objects in our game, and some of them, like certain power switches, should only be used once. After using them, the highlight that marks them as usable disappears and they become unusable. Sounds good, right?

As it turns out, all of this only takes effect when you move your mouse off of them. As long as you keep your cursor in place, you can flip switches all day long. Of course, no one planned for this. Animations break, and (surprisingly!) you can simply switch lights on and off as fast as you like.

Sadly, as fun as this little bug is to play with, it's actually quite serious. Not only are lights important for staying alive, some doors are only open when the power's on. This means that by turning on the first switch, turning it back off, and looking away, the player can effectively soft-lock the game within about a minute of starting.

Since I found the bug, I decided to take it on and dig a little. It looks like the player is the culprit. The player keeps track of nearby interactive objects, adding them to and removing them from a list as you move and look around. When an object is no longer interactive, it can't be added. However, it doesn't get removed when the object's state changes.

There are a couple of ways that I could fix this issue: Removing things from the list, and making the interact key check the object when used. Originally, I was planning to do the former. However, I recently thought of another bug. If an object isn't interactive, and then it changes, the highlight won't appear and the player will be unable to use it. The latter change should fix this issue as well, but I'll have to give it a shot first.

1/25/16

Let's Make a Roguelike - 4 - A basic map

Followup

Last week's shape drawing challenges were mostly an exercise, as it's unlikely you'll need them too much going forward. With that said, I still have the solutions.

Circles

Circles and ellipses can be difficult, but you they become much simpler when you apply just a little bit of trigonometry to them. By taking the sin from 0-180 degrees, we get a smooth, semicircle curve of values. From here, we can use some basic arithmetic to get the right length and position of each horizontal line, drawing similarly to how we handled the rectangle.
   79 void draw_ellipse(WINDOW* win, int row, int col, int height, int width, char shape)
   80 {
   81     int rows, cols;
   82     getmaxyx(win, rows, cols);
   83 
   84     for(int i = clamp(row, 0, rows); i < clamp(row + height, 0, rows); ++i) {
   85         float ratio = (float)i / height;
   86         float w = sin(degtorad(ratio * 180)) * width;
   87         for(int j = clamp(col + ((width - ceil(w))), 0, cols); j < clamp(col + ceil(w), 0, cols); ++j) {
   88             mvwaddch(win, i, j, shape);
   89         }
   90     }
   91 }
I've added two new utility macros, degtorad and radtodeg. This isn't really the place to explain them, as they're exactly the same as the normal mathematical conversions.
The result is decent, although it doesn't always perfectly fit the dimensions given.

Better Rectangles

An empty rectangle is quite simple. By keeping track of the starting and ending bounds (the ones used in the nested loops),you can easily get the border positions. If either iterator is at a border, draw a normal character. If not, don't. To keep the inside empty, you can simply draw a space character. This, in particular, is a really useful trick. If you absolutely need to blank something, you can always just draw a space over it. Here's the code:
   60 void draw_rect(WINDOW* win, int row, int col, int height, int width, char shape, int filled)
   61 {
   62     int rows, cols;
   63     getmaxyx(win, rows, cols);
   64 
   65     int imin = clamp(row, 0, rows);
   66     int imax = clamp(row + height, 0, rows);
   67     int jmin = clamp(col, 0, cols);
   68     int jmax = clamp(col + width, 0, cols);
   69     for(int i = imin; i < imax; ++i) {
   70         for(int j = jmin; j < jmax; ++j) {
   71             char s = shape;
   72             if(!filled && j != jmin && i != imin && j != jmax - 1 && i != imax - 1)
   73                 s = ' ';
   74             mvwaddch(win, i, j, s);
   75         }
   76     }
   77 }
And here's my final screenshot:

A Map

Today, we're going to draw an actual map. We won't be getting to gameplay or proper generation just yet, but I thought this would be a good way to cap off these posts about drawing.

Fundamentals

Let's start with some theory.
There are a few things to keep in mind when dealing with a text-based map, and be trying to address as many of these as I can here.

Tiles and Terrain

First off, you need a way to draw your map. I highly recommend using an array of 'tiles', each with various properties like their color pair and symbol.

Note: Why not shapes?

We've spent a couple weeks playing with various ways of drawing text and shapes, so you might be wondering why we don't just use various shapes for our maps. Sadly, this method results in a couple of problems:
  1. How do you use them? To generate your map, you'll need a pretty fancy algorithm just to handle placing and arranging shapes. After that, it'll take much more tweaking to get something even remotely decent looking. The best solution is simply to arrange rectangles and the like in a grid fashion, which just results in a set of large tiles. This could be done as an aesthetic, but you'll still be using tiles anyway.
  2. How do you know what you're standing on? The player and enemies need to know where walls are, at the very least. Many roguelikes go far beyond this, with multiple types of terrain. With shapes, you'll need to use a bunch of tradition collision detection techniques, but with tiles you can just use a simple array lookup.
  3. It'll be close to impossible to easily allow for terrain modifications, at least not at the level of granularity you'll want. With tiles, changing parts of the terrain is quite simple.
Tiles can be represented as structs, which allows us to add a few extra properties. These can vary from game to game, but could easily include things like terrain cost, contact damage, height, and even handles to scripts that execute when a creature enters! The possibilities are endless, so long as you take care not to go too crazy with the amount of data you use. Remember, you'll probably be storing thousands of them in memory.

Other Objects

Of course, your game would probably be boring without any non-tile objects. Most roguelikes generally have 2 separate kinds of non-tile objects: Items and Entities.
Items are pretty straightforward. Generally, tiles are allowed to have some kind of 'inventory', which is generally just a list of items. When the player picks one up, the item is moved to the player's inventory and removed from the tile's inventory.
Entities, as I call them, are a more general group. They can be anything from static objects like traps, push-able rocks, and statues, to more complex things such as monsters (and in some cases, the player). For static entities, you may simply want to make them into special types of tiles. Nevertheless, other kinds of entities benefit from some more complicated logic.
In many roguelikes, you can generally be assured that only one entity will be on a given tile. If this is the case, you may want to make tiles hold a flag of some kind indicating that they're occupied. This flag can simply force a tile to pretend that it's solid, and any AI you write can handle the rest. However, you should probably store your entities separately from your tiles anyway. Because they usually move around and appear/disappear regularly, they need to be managed more carefully than tiles or items.

Generation

In order to actually have a map, first you need to make one. Since this a roguelike, we'll be generating our map on the fly. We won't be covering any serious procedural generation algorithms just yet, but you need to start thinking early about how you want your maps to look. Are you making dungeons with interconnected rooms? Organic-looking natural caves? Grid-like mine shafts? Continents and outdoor terrain? Modern buildings? Several of the above, or something completely different?
Keep in mind some of the underlying elements of each. When the time comes to write a map generator, you'll probably need to put multiple techniques together to form the layouts you want.

Storage

Storing map data externally is optional. After all, you don't need to implement saving if your game is short. You also might regenerate areas on the fly, and make the constant changes part of the narrative. However, many roguelikes do it., because most games benefit greatly from a basic save system.
In some cases, you can get away with nothing more than literally writing the map's data to a file directly. In others, you'll want some special changes to account for entities and items. This is also up to you, and we'll address this issue much later. For now, it's good to mention for the sake of completion.

Implementation 

As a final treat to this otherwise dry and text-heavy tutorial, let's generate and draw a super-simple map. We won't bother making it look too nice and orderly yet, and instead focus on getting a set of tiles created and drawn for the moment. Here's our generation code:
    1 #include <curses.h>
    2 #include <stdlib.h>
    3 #include "map.h"
    4 
    5 typedef struct tile
    6 {
    7     int tile_char;
    8     int tile_pair;
    9 } tile;
   10 
   11 int map_rows, map_cols;
   12 tile* map_tiles = 0;
   13 
   14 void generate_map()
   15 {
   16     // Set map dimensions to match the screen
   17     getmaxyx(stdscr, map_rows, map_cols);
   18 
   19     // Create a new map, deleting an old one if it exists already
   20     if(map_tiles)
   21         free(map_tiles);
   22     map_tiles = calloc(map_rows * map_cols, sizeof(tile));
   23     for(int i = 0; i < map_rows; ++i) {
   24         for(int j = 0; j < map_cols; ++j) {
   25             map_tiles[i * map_rows + j].tile_pair = rand() % 3;
   26             map_tiles[i * map_rows + j].tile_char = (rand() % 93) + 32;
   27         }
   28     }
   29 }
This is pretty standard with our existing code. One thing you'll find is that many of the basic techniques used earlier come back every once in a while.

To keep track of the map,we use a simple global pointer. By keeping it in our C file, we can generally prevent it from being used elsewhere later. In general, we want to keep our map and tile access to functions whenever possible, and hide the actual structure and implementations in our C file.
Our generation 'algorithm', if you can call it that, takes a random color from the ones we picked and a random visible character, then assigns them to each tile. The result, as you'll see in a moment, is some interesting gibberish.

Next, we draw the map. You'll notice that this is awfully similar to our original rectangle drawing code:
   31 void draw_map()
   32 {
   33     for(int i = 0; i < map_rows; ++i) {
   34         move(i, 0);
   35         for(int j = 0; j < map_cols; ++j) {
   36             attron(COLOR_PAIR(map_tiles[i * map_rows + j].tile_pair));
   37             addch(map_tiles[i * map_rows + j].tile_char);
   38         }
   39     }
   40 }
In time, we'll be making this a bit more complicated. In particular, you might notice that very large maps are slow to draw. This is because curses absolutely must redraw every character, since they're all new. Some curses implementations will avoid redraws when they can, but to avoid dependence, we'll eventually want to make the drawing a bit more efficient. We'll also need cropping/scrolling code at some point, but we don't need either of those for this demo either. Also, I used a new method for selecting colors. attron(COLOR_PAIR(n)) allows you to easily set the current color pair, and attron() on its own can also set other attributes that we'll be covering in the future.
Here's how mine turned out:
Even though our map is nothing more than random garbage, it still has some pleasing qualities to it. The two colors look like they make some kind of abstract pattern, and the randomized characters add some texture to break things up.
These two things are important to remember. We'll be using other kinds of random noise functions (albeit more skillfully) in the future to create interesting maps, and it's often useful to use multiple characters for natural ground to make it more interesting to look at. As a closing piece, here's two screenshots from my last 7drl entry, To the West, demonstrating this very thing:
Keep this in mind, too: I used nothing but good old rand() to make To the West's map, and it still looks decent. Even with the most basic techniques, you can make some really nice-looking areas.

Next Steps

  1. Instead of taking random color pairs and characters, try to pull tiles from a predetermined list of combinations.

Important Terms and Functions

Colors and Text Attributes

  • attron(int attrs) Turns on a particular text attibute.
  • attroff(int attrs) Turns off a particular text attibute.
  • COLOR_PAIR(n) A macro that returns the particular attribute for a given color pair.

Map Terms

  •  Tile A single portion of a map, often rectangular. These are like thew squares in a grid, and hold basic terrain and visual data.
  • Entity An object in the world, usually one that blocks movement and/or is active and can move on its own.
  • Item An object that is held and used by entities. Generally, these are stored separately to entities and cannot act on their own.

Final Code

 Full code: here

1/22/16

Capstone Update 10: A new team, and a new game

With a new semester, it's time for a new weekly Capstone post.

Dungeon Restocker failed to go forward last semester, so I've joined a new team. Since the new production cycle has only just begun, I'm going to take this post to discuss the game and my role for the coming semester.

The Game

First off, let me introduce you to the game, The Last Light, with a short video:

As you can see, this is not the same goofy lighthearted fun that I was working on last semester. In addition to the trailer, the team made a brief description of the game for a Greenlight page:
"The Last Light is a first-person puzzle / survival-horror game where you play as Sophie Thompson, a highschool student on her way to pick up her younger brother Lucas, when an unknown entity causes a widespread power outage. Sophie embarks on a journey through the underground where she learns that there is some invisible entity lurking in the darkness and that the only way to remain safe is to, at all times, stay in the light."
I think that covers the game pretty well. I find the project especially exciting because this is my first time working on a game in this genre. I'm not usually a huge fan of horror games, but I find the premise interesting so I don't mind developing one. Now, let's talk about my role in all of this.

My Job

Like the other senior production games, this one came from last semester. All of the newcomers, like myself, have taken what was once a tiny 4-person team and tripled the headcount. As a result, many of us have been given some specific responsibilities.
In my case, I'm the team's dedicated Graphics Programmer. That means that I'll be handling a lot of the work on visual effects and materials, alongside the artists. While I enjoy graphics programming, I've never gone too deep into the subject. My work on DFEngine is the farthest I've really taken things, so I'm excited to have a real opportunity to develop and practice my skills.

For now, I'm going to leave it at that. Hopefully I'll have more to talk about next week as I start getting my hands dirty!

1/12/16

Let's Make a Roguelike - 3 - Shapes and colors

In a move surprising nobody, I've released this a day late. I'm going to blame AGDQ, and hope that things work out better in the future.

Followup

Off-Center Text

The text is off-center for one simple reason: Even though we're placing the text in the center, the text is still drawn left-aligned. In order to make the text be properly centered, we need to adjust for its width. To do that, we subtract half the length of the text, which we can grab with a simple strlen call. I decided to go a bit further, by letting the user specify the text alignment in my function.

If I wanted to take this even further, I could also add wrapping/ellipsis options, but I decided to leave it here for now:
    7 enum alignment
    8 {
    9     ALIGN_LEFT,
   10     ALIGN_CENTER,
   11     ALIGN_RIGHT
   12 };
   13 
   14 /*
   15  * This function lets the user draw a string to a specified row, with
   16  * automatic alignment. If the chosen row is outside the bounds, or the text is
   17  * too wide, the function draws nothing and returns.
   18  *
   19  * align specifies the alignment of the text, and expects an alignment value as
   20  * declared above.
   21  */
   22 void draw_text_aligned(WINDOW* win, int row, const char* text, int align)
   23 {
   24     int rows, cols;
   25     getmaxyx(win, rows, cols);
   26 
   27     // If we're out of bounds, or we don't have enough space, return
   28     if(row > rows || strlen(text) > cols)
   29         return;
   30 
   31     switch(align) {
   32         case ALIGN_LEFT:
   33             mvwaddstr(win, row, 0, text);
   34             break;
   35         case ALIGN_CENTER:
   36             mvwaddstr(win, row, (cols / 2) - (strlen(text) / 2), text);
   37             break;
   38         case ALIGN_RIGHT:
   39             mvwaddstr(win, row, cols - strlen(text), text);
   40             break;
   41     }
   42 }

Drawing Shapes

Our current title screen is functional, but it's also very plain and boring. Let's draw some shapes to fix that. To keep our code clean, I'm going to add a new pair of source files to the project: draw.c and draw.h. We'll keep some of our basic drawing code in here.

A Single Character

To kick things off, we're going to just draw one character onscreen. We don't really need a proper function for this yet; instead we'll just use the curses function instead. The function in question is addch.

Note

You might notice that this function is using the same naming standards as last part's addstr. Just like addstr, you can add prefixes to move the cursor or change the target window.
Simple as it may be, addch is a very useful function because we can use it to draw whatever we want. At this point, you can draw pretty much any shape just by calling addch in the right places. This will become very important when we start on the game proper, but for now let's add a simple starfield to our title screen as an exercise in using it.

For the sake of convenience, I've put this code together into a function. Let's take a closer look:
   35 /*
   36  * This function draws a bunch of characters onto the screen to simulate stars.
   37  */
   38 void draw_stars(WINDOW* win)
   39 {
   40     int rows, cols;
   41     getmaxyx(win, rows, cols);
   42 
   43     char star_chars[] = { '.', '.', '.', '*', '+' };
   44 
   45     int star_count = rand() % 100 + 50;
   46     for(int i = 0; i < star_count; ++i) {
   47         mvwaddch(win, rand() % rows, rand() % cols, star_chars[rand() % 5]);
   48     }
   49 }
The first couple of rows should look familiar to you by now. We grab the dimensions of the window for later use, and assign them to the usual variables.

Next, for convenience, we make an array with the possible stars that we'll be drawing. To make smaller stars more likely, I've simply made extra spots in the array with periods. We also determine how many stars we want with a simple rand call.

Warning

Don't for get to call srand() at the start of your program! srand() is used to seed the RNG, so if you fail to call it then you'll probably get the exact same random numbers every time you launch the program. You should have to bother calling it constantly either. A single srand(time(NULL)); at the start of your program should do fine.
After that, it's just a matter of choosing a random location and character with rand, and repeating the process until you hit the desired number. Stick the function call before you draw your text, and you'll have get a result like this:
Stunning modern visuals!
We could go particularly crazy with this solution by adding custom noise functions and whatnot, but this will do for a simple title effect.

A Box

Now, we have something apart from our title. However, we can do better. Next, we'll draw a rectangle to represent land.

Note

Curses does have box and border functions, but they're used to draw borders around windows. Since we're still only working with a single window, they're not useful to us just yet.
To do this, we'll add a third drawing function, draw_rect:
   52 /*
   53  * This function draws a filled rectangle with the specified dimensions at the
   54  * specified position. The shape argument determines what character will be the
   55  * fill for the rectangle.
   56  *
   57  * If the rectangle would go out of bounds, it is cut off.
   58  */
   59 void draw_rect(WINDOW* win, int row, int col, int height, int width, char shape)
   60 {
   61     int rows, cols;
   62     getmaxyx(win, rows, cols);
   63 
   64     for(int i = max(row, 0); i < min(row + height, rows); ++i) {
   65         for(int j = max(col, 0); j < min(col + width, cols); ++j) {
   66             mvwaddch(win, i, j, shape);
   67         }
   68     }
   69 }
As always, we grab the window dimensions. Then, we do something a little different. As far as I am aware, the C standard doesn't have any sort of clamping function. Since clamp functions are generally quite useful in a lot of situations, I threw together a handful of simple macros:
    1 #define min(a, b) (a < b ? a : b)
    2 #define max(a, b) (a > b ? a : b)
    3 #define clamp(a, b, c) (min(max(a, b), c))
These are simple to make and provide a lot of convenience, so I recommend keeping them somewhere easily accessible. I've added them to a new header, math_util.h, so that I can reuse them later. Here, I'm just using them to keep the rectangle within the bounds of the window.

One nice thing about addch-based drawing functions is that most of them end up being pretty similar: A constrained, finite loop with a single mvaddch or mvwaddch inside it.

With all the drawing functions done, my new main function looks like this (Some parts have been skipped)
    1 #include <curses.h>
    2 #include <stdlib.h>
    3 #include <time.h>
    4 #include "draw.h"
  ... 
    9 int main(int argc, char* argv[])
   10 {
   11     // Seed the RNG
   12     srand(time(NULL));
  ... 
   34     draw_stars(stdscr);
   35     draw_rect(stdscr, rows / 2 + 3, 0, rows / 2 - 3, cols, '#');
   36 
   37     // Print the title, then wait for a button press before exiting
   38     const char* title_str = "Cool Game Title";
   39     draw_text_aligned(stdscr, 3, "Cool Game Title", ALIGN_CENTER);
   40     draw_text_aligned(stdscr, rows / 2, "Press any key to continue", ALIGN_CENTER);
   41     getch();
   42 
   43     // End Curses
   44     endwin();
   45     return 0;
   46 }
This puts our rectangle just below our "Press any key" prompt.

Adding Color

We've already made some progress towards a nice title screen, but our text is starting to get harder to read. If we add much more detail, the problem will only get worse from here. It's time to add some color.

Color Pairs

As I mentioned in the first part of this series, curses handles colors in foreground/background pairs. curses keeps track of both colors and pairs with ids, rather than storing RGB values directly. It's up to the developer to decide what each pair contains, and what each color actually is (assuming the terminal supports color changes).

When doing more complex things with color, it's important to know how to manage your color pairs. I'm not going to cover that just yet, however. For now, we'll cover the basics of actually setting up and using color in curses.

Checking for Color

The first step, obviously, is to ensure that color actually works. Just like with terminal dimensions, we need to check what our target supports in terms of color.

Unlike terminal dimensions, we can still make things work if we don't have color support. It's up to you if you want to actually support monochrome or low-color displays, but it's probably easier than low resolutions.

Testing a terminal's color capabilities is pretty simple. There are four main things to check: color, color changing, color count, and color pair count. Take a look below.
    8 #define MINIMUM_COLORS    16
    9 #define MINIMUM_PAIRS     16
  ... 
   36     // Test the color capabilities of our terminal
   37     if(!has_colors()) {
   38         endwin();
   39         fprintf(stderr, "Error: Your terminal does not support color output.\nThis application must have color output to run.\n");
   40         return 1;
   41     }
   42 
   43     start_color();
   44 
   45     // If they support color but lack full support, warn them and continue
   46     if(!can_change_color()) {
   47         endwin();
   48         fprintf(stderr, "Warning: Your terminal cannot change color definitions.\nThis may impact the game's aesthetic.\nPress any key to continue.\n");
   49         getch();
   50         refresh();
   51     }
   52     if(COLORS < MINIMUM_COLORS || COLOR_PAIRS < MINIMUM_PAIRS) {
   53         endwin();
   54         fprintf(stderr, "Warning: Your terminal lacks sufficient color support to use this game's full range of color.\nThis may impact the game's aesthetic.\nPress any key to continue.\n");
   55         getch();
   56         refresh();
   57     }
The first block is where I test if colors are supported, using the has_colors function. I've decided that (for now) I'll be requiring some kind of color support, but that's all. After that, the start_color function initializes color support, which we'll need for the latter checks.

can_change_color, as the name implies, simply checks if we can change the values attached to the color ids. This is mostly necessary if you plan on assigning specific values to each color, which we won't be doing this time. Nevertheless, we should check it for future reference.

The final check is for the number of colors and pairs available. I consider this to be the most important, after checking color support. This number will ultimately dictate the size of your palette, and thus just how many colors you can push to the screen. This check is last, however, because COLORS and COLOR_PAIRS is unset until you call start_color.

Note

If you're developing on linux (and possibly Mac, but I haven't tried), then you can test these checks easily by setting the TERM environment variable before starting your application. This variable informs curses of what kind of terminal it's running in, and different terminal profiles have support for different color settings.

Using Color

Now that we've checked our color capabilities, let's finish off with a little demonstration.
The two main functions that we'll be using to handle colors are init_pair and color_set. The names of these two functions is pretty self-explanatory, so I'm going to jump back into the code to explain their use.
   59     init_pair(1, COLOR_YELLOW, COLOR_BLACK);
   60     init_pair(2, COLOR_GREEN, COLOR_BLACK);
   61 
   62     color_set(1, 0);
   63     draw_stars(stdscr);
   64     color_set(2, 0);
   65     draw_rect(stdscr, rows / 2 + 3, 0, rows / 2 - 3, cols, '#');
   66 
   67     // Reset our color
   68     color_set(0, 0);
   69 
   70     // Print the title, then wait for a button press before exiting
   71     const char* title_str = "Cool Game Title";
   72     draw_text_aligned(stdscr, 3, "Cool Game Title", ALIGN_CENTER);
   73     draw_text_aligned(stdscr, rows / 2, "Press any key to continue", ALIGN_CENTER);
The init_pair takes the id of the pair being set, followed by the foreground color and then the background color. For our convenience, curses provides macros representing several basic colors: COLOR_BLACK, COLOR_WHITE, COLOR_RED, COLOR_GREEN, COLOR_YELLOW, COLOR_BLUE, COLOR_MAGENTA, and COLOR_CYAN.
These come out to be the 8 basic colors available in most terminals. Other colors are often present, but since they're not guaranteed the user is expected to use their actual id number.

color_set isn't the only way to set the color being used, but it's easily the most convenient. The first number passed is the color pair's id, and the second is null. The second argument is actually a void pointer, but it's currently unused by the spec so we set it to 0.


Now, we've discussed the general drawing process when working with curses. You start off the program by building your palette with init_pair. Then you use color_set to select a color pair for use. Finally, you use functions like addch and addstr to place characters onscreen. If you've been following along, your title screen will probably look something like this:
The finished screen, complete with color
Next time, we'll be finally adding something more beyond this title screen.

Next Steps

  1.  Try making a function that draws circles and ellipses. Can you add a moon and clouds?
  2. Add support for empty rectangles to draw_rect. Then, try making a box surrounding all of your text. How do you ensure that the box has no stars in it?
  3. Make your existing draw functions take color input.
  4. Use your existing code to play around with colors and characters. Try to decide how you want things to look.

Important Terms and Functions

Output

  • addch(const chtype ch) Adds a character to at the cursor position and advances it. ch can be treated as a normal character, but can have some other attributes OR'ed to it as well.

Colors

  • start_color() Initializes color in curses, allowing it to be used.
  • has_color() Checks if the terminal supports color output.
  • can_change_color() Checks if the terminal supports changing the RGB values of colors.
  • init_pair(short pair, short f, short b) Sets the pair with id pair to use color f as the foreground and b as the background. If the pair is in use, all occurrences of it onscreen change as well.
  • color_set(short color_pair_number, void* opts) Sets the color pair to be used. opts must equal NULL.
  • COLORS The number of unique colors supported by the terminal. Set by init_color().
  • COLOR_PAIRS The number of unique color pairs supported by the terminal. Set by init_color().

Other

  • srand(unsigned int seed) Seeds the RNG for future rand calls.
  • rand() Returns the next pseudo-random number in the sequence seeded by srand.

Final Code

 Full code: here

draw.c:
    1 #include <string.h>
    2 #include <stdlib.h>
    3 #include "draw.h"
    4 #include "math_util.h"
    5 
    6 /*
    7  * This function lets the user draw a string to a specified row, with
    8  * automatic alignment. If the chosen row is outside the bounds, or the text is
    9  * too wide, the function draws nothing and returns.
   10  *
   11  * align specifies the alignment of the text, and expects an alignment value as
   12  * declared above.
   13  */
   14 void draw_text_aligned(WINDOW* win, int row, const char* text, int align)
   15 {
   16     int rows, cols;
   17     getmaxyx(win, rows, cols);
   18 
   19     // If we're out of bounds, or we don't have enough space, return
   20     if(row > rows || strlen(text) > cols)
   21         return;
   22 
   23     switch(align) {
   24         case ALIGN_LEFT:
   25             mvwaddstr(win, row, 0, text);
   26             break;
   27         case ALIGN_CENTER:
   28             mvwaddstr(win, row, (cols / 2) - (strlen(text) / 2), text);
   29             break;
   30         case ALIGN_RIGHT:
   31             mvwaddstr(win, row, cols - strlen(text), text);
   32             break;
   33     }
   34 }
   35 
   36 /*
   37  * This function draws a bunch of characters onto the screen to simulate stars.
   38  */
   39 void draw_stars(WINDOW* win)
   40 {
   41     int rows, cols;
   42     getmaxyx(win, rows, cols);
   43 
   44     char star_chars[] = { '.', '.', '.', '*', '+' };
   45 
   46     int star_count = rand() % 100 + 50;
   47     for(int i = 0; i < star_count; ++i) {
   48         mvwaddch(win, rand() % rows, rand() % cols, star_chars[rand() % 5]);
   49     }
   50 }
   51 
   52 /*
   53  * This function draws a filled rectangle with the specified dimensions at the
   54  * specified position. The shape argument determines what character will be the
   55  * fill for the rectangle.
   56  *
   57  * If the rectangle would go out of bounds, it is cut off.
   58  */
   59 void draw_rect(WINDOW* win, int row, int col, int height, int width, char shape)
   60 {
   61     int rows, cols;
   62     getmaxyx(win, rows, cols);
   63 
   64     for(int i = clamp(row, 0, rows); i < clamp(row + height, 0, rows); ++i) {
   65         for(int j = clamp(col, 0, cols); j < clamp(col + width, 0, cols); ++j) {
   66             mvwaddch(win, i, j, shape);
   67         }
   68     }
   69 }