Enjoy the holidays with this ASCII fireplace
There’s nothing quite like sitting in front of a warm fireplace over the holidays. And because it’s the holidays, I wanted to share that experience with you.
I like working on FreeDOS, an open source implementation of the classic DOS operating system. We can use OpenWatcom C on FreeDOS to make a passable “fireplace” animation, with the extended ASCII characters from DOS Code Page 437 and the text window feature of DOS console mode programming.
The basics of writing this program are this: We’ll animate our “fire” by defining a “window” that’s one column wide, then print a string of several characters in it. With a narrow window, DOS will “wrap” the text at the window—an easy way to quickly generate a column of characters to simulate “fire.” If we select random offsets for the fire, we can make the “fire” different heights, just like real flame. We’ll define the “fire” using a series of shaded characters, from “light” to “dark,” in either orange or yellow.
Random values for random heights
We’ll need to generate a series of random numbers to simulate the different heights of the flames. In Linux, the best way to generate random numbers from the Linux kernel is with the getrandom
system call. But FreeDOS doesn’t have a system call like this; instead, we need to use the rand
pseudo-random function from the standard C library.
To use rand
, you need to seed the random number generator at a starting point. The best way to pick a seed is to use the system time, using the time
function, which returns the current time as the number of seconds since the epoch. Having set the seed appropriately, every call to rand
will generate a pseudo-random number between zero and some maximum value. Here’s a demonstration:
#include <stdio.h>
#include <stdlib.h> /* rand */
#include <time.h> /* time */
int main()
{
int i;
srand(time(NULL));
for (i = 0; i < 10; i++) {
printf("%d\n", rand());
}
return 0;
}
If you compile this program as random.c
and run it, you should see ten random numbers:
C:\SRC> WCL -q random.c
C:\SRC> RANDOM
3966
5350
3368
23404
21324
10824
567
16513
5861
20438
Draw a window
Using text windows in DOS is a common way to control the screen. Text windows are defined by video memory, and allow you to define an area of the screen to work in. You can clear just this area of the screen, and even set a new background color for it.
Be careful when printing text in a window, however. If your text reaches the end of the window, it will “wrap” immediately to the next line. This is not what you usually want. Except our “fire” program is a special case, where we can use this feature to quickly draw a column of text:
#include <stdio.h>
#include <conio.h>
#include <graph.h>
int main()
{
_setvideomode(_TEXTC80);
_settextwindow(5, 40, 20, 40);
_setbkcolor(1); /* blue */
_clearscreen(_GWINDOW);
_outtext("Hello, world!");
getch();
_setvideomode(_DEFAULTMODE);
return 0;
}
This program uses functions specific to OpenWatcom C; you may need to use different functions if you prefer another C compiler on DOS. The functions to define a text window and set colors are defined in OpenWatcom’s graph.h
header file. The program also uses getch
to wait for a keypress; this function is defined in OpenWatcom’s conio.h
header file.
This program sets the video mode to text mode using color (instead of monochrome) with 80 columns and 25 rows. Then, the program creates a text window starting at row 5, column 40 in the upper-left to row 20, column 40 in the lower-right; this is a 1-column window with 16 rows. The program sets the background color to 1
, which is blue in the DOS color palette, then clears the window, which sets the background color for the window.
Using the _outtext
function, the program then prints a string in the window, then waits for the user to press a key before resetting the video mode back to the defaults and exiting to DOS.
Random flames
We can leverage the 1-column window to draw a string of characters to the screen. To simulate “flames” in text mode, we can use the extended ASCII characters in DOS Code Page 437: characters 0xb0, 0xb1, and 0xb2 are three different levels of shaded boxes, from light to dark, and 0xdb is a completely filled box. If we set the text color to either orange or yellow, these four characters can simulate a flame.
We can use OpenWatcom’s _outmem
function to print out only a few characters from a string. This is like OpenWatcom’s _outtext
function, but _outtext
prints an entire string, while _outmem
lets us specify how many characters to print. Let’s define a 7-character array that starts with three spaces (I’ve used the characters 1, 2, and 3 so we can see them) then includes the four “flame” characters:
char fire[7] = { '1', '2', '3', 0xb0, 0xb1, 0xb2, 0xdb };
If we used _outmem(fire, 4)
, we would always print the first four characters from the array. To print from some arbitrary starting point in the array, we need to use a pointer in the array. For example, if char *f
is a character pointer, we can set the value of f
to the fire
array, then advance the pointer a certain number of positions. Moving ahead by three positions sets the f
pointer to the fourth element in the fire
array, which is 0xb0
. Printing four characters from f
prints the flame characters:
char fire[7] = { '1', '2', '3', 0xb0, 0xb1, 0xb2, 0xdb };
char *f;
f = fire;
f += 3;
_outmem(f, 4);
This array “trick” makes it possible to simulate fire. To draw a “flame,” we set the array position in f
ahead by up to three characters from the flame
array, then print four characters from f
. If the offset is zero, we print three spaces, then the first shaded box. If the offset is one, we print two spaces, then the first two shaded boxes. And so on for up to three positions.
We can pick a random starting position using rand()
and ensuring the value is in the range 0, 1, 2, or 3. One way to do this is with the modulo (%
) operator, like rand() % 4
to return a value from 0 to 3. But if you look at the binary representation of 3, it’s binary 0000 0011, so using a binary operation of rand() & 3
will “mask” the first two bits, giving a final value that’s 0, 1, 2, or 3.
Putting it all together, we can define a text window that’s one column “wide” and five rows “tall,” and _outmem
will print four characters from a random starting point. Because the window is one column wide, the text will “wrap” all the way down the text window. We need to set the text window to be one row “taller” than the array so the text doesn’t scroll out of the window.
#include <stdio.h>
#include <stdlib.h> /* rand */
#include <time.h> /* time */
#include <conio.h>
#include <graph.h>
int main()
{
int col, n;
char fire[7] = { '1', '2', '3', 0xb0, 0xb1, 0xb2, 0xdb };
char *f;
srand(time(NULL));
_setvideomode(_TEXTC80);
for (col = 1; col <= 80; col++) {
_settextwindow(5, col, 9, col);
n = rand() & 3; /* binary: 0, 1, 2, or 3 */
f = fire;
f += n; /* advance ptr */
_outmem(f, 4); /* print */
}
getch();
_setvideomode(_DEFAULTMODE);
return 0;
}
If we save this program as fire.c
and compile it with OpenWatcom, we’ll see a random mix of characters across the screen, each displayed as four characters in 80 columns:
The numbers are placeholders for spaces, and demonstrate that we’re only printing up to four characters from random starting positions in the fire
array.
Animating a fire
To animate a fire, we need to make a few small changes to the program: We’ll randomly pick either orange (color 6) or bright yellow (color 14) to print each column of “flames.” One way to do this is with a simple if
statement that tests if a random number is odd (in theory, half of the time):
if (rand() & 1) {
_settextcolor(6); /* orange */
}
else {
_settextcolor(14); /* br yellow */
}
Also, we’ll continue to draw the flames until the user presses a key; we can use kbhit
from conio.h
to do that. The delay()
function from i86.h
can add a short delay after each loop, so the fire doesn’t “burn” quite as quickly.
do {
...
delay(100);
} while (kbhit() == 0);
We’ll also change the “numbers” in the fire
array to spaces. This lets us skip the extra step of clearing the text window before we draw to it; the spaces will effectively erase whatever text was there before.
#include <stdio.h>
#include <stdlib.h> /* rand */
#include <time.h> /* time */
#include <conio.h>
#include <graph.h>
#include <i86.h> /* delay */
int main()
{
int col, n;
char fire[7] = { ' ', ' ', ' ', 0xb0, 0xb1, 0xb2, 0xdb };
char *f;
srand(time(NULL));
_setvideomode(_TEXTC80);
_displaycursor(_GCURSOROFF);
do {
for (col = 1; col <= 80; col++) {
_settextwindow(5, col, 9, col);
if (rand() & 1) {
_settextcolor(6); /* orange */
}
else {
_settextcolor(14); /* br yellow */
}
n = rand() & 3; /* binary: 0, 1, 2, or 3 */
f = fire;
f += n; /* advance ptr */
_outmem(f, 4); /* print */
}
delay(100);
} while (kbhit() == 0);
if (getch() == 0) { getch(); }
_setvideomode(_DEFAULTMODE);
return 0;
}
This version of the program also hides the cursor with _GCURSOROFF
.
If you compile and run the new program, you can relax in front of an ASCII “fire” in FreeDOS: