General Observations
Welcome to code review, nice first question. I would leave the licensing information out since stack exchange uses the Creative Commons Attribution-ShareAlike license.
The comment block at the top is rather helpful otherwise. FYI, this compiles fine on Windows 10 in Visual Studio 2019 Professional, but doesn't link (usleep()
is undefined).
Some of the code is a little difficult to read as there is vertical crowding, you might want to insert a few blank lines inside main()
between logic blocks.
Avoid Global Variables
It is very difficult to read, write, debug and maintain programs that use global variables. Global variables can be modified by any function within the program and therefore require each function to be examined before making changes in the code. In C and C++ global variables impact the namespace and they can cause linking errors if they are defined in multiple files. The answers in this stackoverflow question provide a fuller explanation.
Declare and Initialize the Variables Where They are Needed
In the original version of C variables needed to be declared at the top of the logic block where they were used, this is no longer true, declare them as necessary.
void main(void) {
char inst[] =
{
'>',
'<',
'^',
'V'
};
system("clear"); //clear screen
srand(time(NULL)); //initialize random generator; TODO: port to C++ and use TRNG instead
printf("\e[?25l"); //disable cursor
//TODO: Implement program input and conditional commands
unsigned char tape[TAPE_SIZE_X][TAPE_SIZE_Y];
int x = 336;
int y = 67;
while (1) {
...
}
}
Let the Compiler Do More of the Work
In the code example above there is no size defined for the array inst[]
, the compiler will fill this in based on the number of initialization values in the array. This makes it easier to write and maintain the code since the size doesn't need to be modified each time an instruction is added.
Magic Numbers
There are Magic Numbers in the main()
function (336 and 67), it might be better to create symbolic constants for them to make the code more readable and easier to maintain. You already define symbolic constants for TAPE_SIZE_X
and TAPE_SIZE_Y
the code would be more readable and understandable if the initialization values for x
and y
used symbolic constants, in this case you could possibly make the initialization half of the tape size for x
and y
.
Numeric constants in code are sometimes referred to as Magic Numbers, because there is no obvious meaning for them. There is a discussion of this on stackoverflow.
Complexity
The function main()
is too complex (does too much). As programs grow in size the use of main()
should be limited to calling functions that parse the command line, calling functions that set up for processing, calling functions that execute the desired function of the program, and calling functions to clean up after the main portion of the program.
There is also a programming principle called the Single Responsibility Principle that applies here. The Single Responsibility Principle states:
that every module, class, or function should have responsibility over a single part of the functionality provided by the software, and that responsibility should be entirely encapsulated by that module, class or function.
There are at least multiple functions in main()
.
- //TODO: Implement program input and conditional commands
- Set up the screen
- The contents of the
while (1)
loop also seem to be multiple functions
Default Cases for switch
Statements
While it isn't necessary in the current code, to prevent undefined behavior it is best to have a default case for every switch statement:
switch (inst[rand() % 4]) { //MOVE
case '>':
x = x + 1;
break;
case '<':
x = x - 1;
break;
case '^':
y = y + 1;
break;
case 'V':
y = y - 1;
break;
default :
printf("Unknown instruction in switch\n");
return 1;
}
Possible Optimization
The second switch
statement could be replaced with a table of values and that would improve performance.
char* control_sequence[] =
{
"\x1B[40m ",
"\x1B[41m ",
"\x1B[43m ",
"\x1B[42m ",
"\x1B[46m ",
"\x1B[44m ",
"\x1B[45m ",
"\x1B[47m ",
};
printf("%s", control_sequence[tape[x][y]]);
fflush(stdout); //flush stdout to prevent abnormal lag in printing to screen
Readability and Maintainability
As mentioned in the general observations, this section of code is very hard to read and even harder to maintain:
if (x >= TAPE_SIZE_X) x = 0; //TOROID CODES FOR OOB CURSOR
else if (x < 0) x = TAPE_SIZE_X - 1;
if (y >= TAPE_SIZE_Y) y = 0;
else if (y < 0) y = TAPE_SIZE_Y - 1;
tape[x][y] = tape[x][y] + 1; // ADD 1
if (tape[x][y] == 8) tape[x][y] = 0;
usleep(16667); // CAP SPEED TO 60FPS : 1/60 seconds = 16666.666... microseconds
printf("\33[%d;%dH", y, x); //print @ cursor position
In languages like C, C++ or Java put action code such as x = 0;
on its own line, preferably in braces, that makes the action easier to find, and with the braces easier to maintain by adding more statements later:
if (x >= TAPE_SIZE_X)
{
x = 0;
}
else if (x < 0)
{
x = TAPE_SIZE_X - 1;
}
if (y >= TAPE_SIZE_Y)
{
y = 0;
}
else if (y < 0)
{
y = TAPE_SIZE_Y - 1;
}
tape[x][y] = tape[x][y] + 1; // ADD 1
if (tape[x][y] == 8)
{
tape[x][y] = 0;
}
usleep(16667); // CAP SPEED TO 60FPS : 1/60 seconds = 16666.666... microseconds
printf("\33[%d;%dH", y, x); //print @ cursor position
An example of how hard the original code is to read is that I missed the magic number 8
in the discussion of magic numbers above.