← Blog Home

Tips & Tricks 1: Debugging with print() Statements

A lot of times when writing code you will have something that doesn't error but doesn't work either. These, very simply, are bugs in your code.

TL;DR: When your code isn't working but you don't know why, add print() statements to every unique code block and figure out exactly which lines of code are and, more importantly, are not running! Use these print()s to check what your variables are as well.

This article will be about how to use print() as your most basic tool for "debugging" your code!


Bugs in Code

Most of the time, bugs happen because your code's "logic" is incorrect or inconsistent. Code "logic", or the "flow" of a program, determines what specific statements run and which do not.

The most basic "logical operation", typically called a control statement is the if statement:

if someExpression then
    print("it's true!")
elseif someOtherExpression then
    print("It wasn't but something else was!")
else
    print("neither were true!")
end

Based on the values of someExpression and someOtherExpression, only one of those three print() statements will run, along with any other code in those blocks.


Introducing print()

Where debugging with print() comes in is when the logic isn't working like you think it should be. We all write code that we think works, and something proves us wrong, so we go back and change it. Figuring out what to change is an important and sometimes difficult process.

Take, for example, this if statement which is part of checking if a move is valid in chess:

-- to is the square the piece is moving to
-- from is the square the piece is moving from
if from.HasPiece and to.HasPiece and from.Piece.Color ~= to.Piece.Color then
    -- capture piece
elseif from.Piece.Color == to.Piece.Color then
    -- invalid move, throw an error
    return
else
    -- move piece to open square
end

A savvy programmer will notice a few bugs in that code without needing to write any debug print()s, but can you?


While testing the chess game, you notice that you can't actually move pieces to open spaces! Instead, you get an error: attempt to index 'Piece', a nil value, on line 5! If you go a little further, you might also notice that you get the same error if you try to move a piece from an empty tile, no matter what the target tile is!

Line 5, elseif from.Piece.Color == to.Piece.Color then shouldn't be erroring, you think. But it is, and you know that either from.Piece or to.Piece don't exist when this line runs in those two known conditions. Again, a savvy programming would notice immediately what the problem is, but let's assume (or for some of you this might still be true!) you don't know what's going on.

Rather than make random edits to that line to try and get the code working, we first need to figure out what is working. One of the most useful ways to do this is print() statements holding some debug info.

Specifically, we should start by adding them to every unique code block:

print("code is running")
print("from.HasPiece", from.HasPiece)
print("to.HasPiece", to.HasPiece)

if from.HasPiece and to.HasPiece and from.Piece.Color ~= to.Piece.Color then
    -- capture piece
    print("Capturing piece" .. to.Piece.Name)
elseif from.Piece.Color == to.Piece.Color then
    print("Invalid move!")
    -- invalid move, throw an error
    return
else
    print("Moving piece")
    -- move piece to open square
end

The point of this is two-fold: following the computer as it executes the code, and checking the values of our variables at various points in the code.

For something this simple, all we do is validate what we know: Capturing piece prints correctly, Invalid move! prints when trying to move pieces of the same color on top of each other, but the same line errors, the elseif, when trying to move a piece.

However, we also see the values of from.HasPiece and to.HasPiece! The line only errors when one or both of them is false!

Since we know Capturing works fine, we can remove that print() entirely. However, we will leave the other two. Moving piece because it does not fire at all, the variable prints because our error only happens based on their values and Invalid move! because, for some reason, it is the branch that is erroring.

This process of removing the prints as you verify what code is working until you get down to only a few that don't work correctly should be after putting them in in the first place. If you don't have enough prints, you won't be able to see what the code is doing and will be no closer to fixing the problem. There's no fee for adding or removing them, so use what looks like too many to start with and remove them as you narrow down the bug!


Fixing the Bugs

Look closely at the line that is erroring:

elseif from.Piece.Color == to.Piece.Color then

Using the print()s we added, we know that this only errors when HasPiece is false, meaning that Piece is nil for that square (either to or from).

And this makes perfect sense! The very first thing we try to do is check a property of Piece! Additionally, when we're trying to move a piece, it's actually to.Piece erroring, for the same reason!

You may have written the code thinking that this elseif wouldn't run if the original if statement found that the pieces didn't exist, because we do check for it there, or you may simply have forgotten to check. As you can see, assumptions aren't always correct!


By adding those two checks to the elseif expression, all of the prints are now working!

print("from.HasPiece", from.HasPiece)
print("to.HasPiece", to.HasPiece)

if from.HasPiece and to.HasPiece and from.Piece.Color ~= to.Piece.Color then
    -- capture piece
elseif from.HasPiece and to.HasPiece and from.Piece.Color == to.Piece.Color then
    print("Invalid move!")
    -- invalid move, throw an error
    return
else
    print("Moving piece")
    -- move piece to open square
end

However, they're not correct! Moving piece will print (and potentially a line in that block will error!) when from.HasPiece is false, even if a piece is present at to!

Following the same process as before, we simply need to check that from has a piece and to doesn't:

print("from.HasPiece", from.HasPiece)
print("to.HasPiece", to.HasPiece)

if from.HasPiece and to.HasPiece and from.Piece.Color ~= to.Piece.Color then
    -- capture piece
elseif from.HasPiece and to.HasPiece and from.Piece.Color == to.Piece.Color then
    -- invalid move, throw an error
    return
elseif from.HasPiece and not to.HasPiece then
    print("Moving piece")
    -- move piece to open square
end

Again, this introduces a new bug! If the code after this if statement relied on the fact that the move being made was, in fact, valid, if from doesn't have a piece, this will still error!

This can, of course, can be fixed by adding back an else branch that is identical to the first elseif, or we can check in the first elseif if from.HasPiece is false:

elseif not from.HasPiece or (from.HasPiece and to.HasPiece and from.Piece.Color == to.Piece.Color) then

Either way works fine, but having them separate allows you to have different "errors" to tell the client without any additional if statements.


Now that we know all the logic works, we can reorganize it some using nested if statements to avoid repeating from.HasPiece and to.HasPiece so much!

if from.HasPiece then
    if to.HasPiece then
        if from.Piece.Color == to.Piece.Color then
            -- invalid move, throw an error
            return
        else --if from.Piece.Color ~= to.Piece.Color
            -- capturing piece
        end
    else --if not to.HasPiece
        -- move piece to open square
    end
else --if not from.HasPiece
    -- invalid move, throw an error
    return
end

While this does take up more lines of code, in my opinion this better because it is easier to understand the "flow" of the code when the logic is written out in a nested fashion like this, as it more closely represents my thought process when I am checking if a move is valid in a real game of chess.


TL;DR

  • Add more print()s than you think you need, to every unique code block.
  • print() the values of variables used in your controlling logic.
  • When you know a section of code is working, remove the print()s for that section.
  • You're done when you have no print()s left to remove!

About the Author

Valentine Albee - ROBLOX Developer and Professional Programmer

Feel free to shoot me a message on Discord!
Ideas and questions are always welcome. :)

Posted in Scripting Tips

Commentary

Leave a Comment

Ad