OK, first thing, as you noticed, yes, that's an awful lot of if checks! It's good that you're starting to get a "feeling" for when there is a lot of redundancy. Because that's what's going on here: you're doing something over and over again, when it really can be considered as just one process.
More precisely, you can think about it in terms of a question:
For every row R,
for every column C,
is there something in location R/C?
--> Yes: print its letter
--> No: print space
Often what I do when I code, especially things like this that can be decomposed naturally, is what's called top-down coding. I'll write out the functions that have sub-functions, and I'll only worry about the sub-functions once I'm done with the upper level of functions. Not only does this help write clean code, but it also helps me think about the problem precisely. Too often if I just jump right in, I end up with something albeit more-or-less working but usually messy.
Your case is a very good example. As you said, you
could do it by simply cascading a whole bunch of if checks. But that's really not the most elegant way of doing it. As I said above, a function can be thought of as a question, of sorts. Worry about getting the top-level logic figured out.
Then you can worry about implementing the sub-functions (what I called the helper functions).
You might notice, if you think ahead a bit, that having functions to tell you what is at location x/y will be really,
really,
really useful later on, for instance when you want to start playing around with moving things from square to square (and checking if the square is occupied already). This is a big benefit of decomposing your functions: chances are, you will be able to use the
exact same function later on, thus avoiding the writing of a whole new function!
I think you pretty much understand the general idea of the top-level logic. What you need to get your head around now is how to loop through the lists of objects and see if one of them has coordinates "x"/"y". To do so, you will need to understand (at least the basics of) linked-list theory. Your books will provide a more in-depth explanation than I can do here, but basically here is how they work:
Node1 --(next)--> Node2 --(next)--> Node3 --(next)--> null
|-data |-data |-data
The idea is that every node contains a 'next' pointer and some data. The next pointer indicates what the next element of the list is. When you reach a next pointer that is null, you know that you have reached the end of the list. (That is what the code I showed you was doing.) The data contained in the node represents whatever you have a linked list of (in this case, objects and characters).
Side note: SMAUG doesn't implement linked-lists according to theory, because it puts next and data in the same level. This leads to problems because an object must have multiple list pointers: next_content, next_carried, etc. It is cleaner to have objects called something like "linked list node" that have a next pointer and a data pointer, where next points to another node and data points to the actual OBJ_DATA structure.
End side note.
In any case, you are indeed very close -- keep it up! Give a think over what I've written and let me know if it makes sense. For now I'm going to get some sleep. :-)