This was originally posted on Google+, which has now been shut down. It was helpfully converted to Markdown by Robert Jacobson after which I adjusted it for reposting here. I have dated it at its original posting date, but it was posted here on 2019-09-07.
Since the last time, the nasm people have been able to fix the bugs I stumbled upon, and pushed them in a release.
Anything newer than 2.10.6 is probably also fine. Please note that you have to update if you are following this tutorial on OS X, because we will be triggering the bugs present in earlier versions.
With that out of the way, let's dive into making the game real time, so you don't have to hold down buttons on the keyboard. This should serve as a good repetition after a long hiatus, because we will only be doing more of the stuff we have already learned.
Step 13: ANSI escape codes
This part is for you, Michael!
When playing the latest version of the game we are making, we would fill the scrollback buffer in the terminal quite quickly with uninteresting prints of former game states. What we want to do instead is to print new versions of the board on top of the previous version. To do this, we will use an ANSI escape code sequence to move the cursor up to the top of the board each time we print it.
We want to use the CSI code CUU; Cursor up. The structure for CUU is ESC, '[', number of rows to move upwards, 'A'. ESC is the magic value 27 = 0x1b = 033. The number of rows we want to move is height + 2, counting the top and bottom solid lines in addition to the height number of open lines:
move_up db 0x1b, '[27A' ; Keep number in string equal to height + 2
We can put this right above our definition of board, and then print the entire thing in one syscall. We also need the size. Let's put this calculation right under board_size:
move_up_then_board_size equ $-move_up
If we print both of move_up and board instead of just board, it will finally stand still and not scroll back:
mov rsi, move_up ; buf mov rdx, move_up_then_board_size ; nbyte
In addidtion, we need to print it once without move_up before entering main_loop, so we avoid overwriting the user's terminal history. This is easily accomplished by sticking an additional syscall to SYS_write just before main_loop.
Your program should now look like part1.asm.
Step 14: Velocity
Next we will get some velocity going, so that the snake keeps moving in the direction it's going. We will do this by replacing the inc, dec, add and sub instructions in our cmp/jne block by storing the appropriate velocity in r15 with appropriate mov r15,
Hopefully, you will end up with something like part2.asm.
Step 15: Nonblocking input
Even though we are in half-cooked (sigh) mode, we are still in blocking mode. The SYS_read call blocks until it can read at least one byte. Let's finally change that. We will do that by setting and unsetting the O_NONBLOCK flag on STDIN_FILENO with SYS_fcntl:
SYS_fcntl equ 0x02000000 + 92 F_SETFL equ 0x00000004 O_NONBLOCK equ 0x00000004 mov rax, SYS_fcntl mov rdi, STDIN_FILENO mov rsi, F_SETFL mov rdx, O_NONBLOCK ; 0 to unset syscall
Unfortunately, even though we set this flag specifically on STDIN_FILENO, it magically applies to standard out as well. We don't need or want to handle nonblocking output, so we opt for the cheap workaround of setting and unsetting O_NONBLOCK before and after calls to SYS_read. We'll set the flag just before .read_more: and unset it between .done: and jmp .main_loop. Running the game now, it seems to still be kind of blocking, at least until we press one key. This is because we currently do not attempt to tell the difference between having read one byte and not. Let's fix that.
Begin by removing the "Assume exactly one byte was read"-comment. In man 2 read we see that the count of bytes read will be returned, and from the all-mighty ABI we learn that such a return value will be passed in rax. When no bytes are read because of O_NONBLOCK, SYS_read will actually return -1 and claim that the error EAGAIN has occurred. The simplest conclusion we can draw from this is that we check rax and jump to .done if it's not 1:
cmp rax, 1 jne .done
Your game should now be incredibly unplayable, and look something like part3.asm.
Step 16: Real time
At this point we need some kind of real time game loop. However, it is easier to cheat and just sleep for a bit between each frame. The price we have to pay for going the easy route is that we have to implement sleep in terms of select, since sleep is not available directly as a syscall.
SYS_select equ 0x02000000 + 93
From man select:
int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* errorfds, struct timeval* timeout);
nfds, readfds, writefds, and errorfds can all be 0. That should fill up rdi, rsi, rdx and rcx. timeout must point to a valid timeval struct instance, its address placed in r8. Oh, nuts. Since we are already using r8, this is bad news. Let's just replace all instances of r8 with r14 to avert immediate disaster.
The declaration of struct timeval is messy and ridden with preprocessor noise, but it boils down to this nasm:
struc timeval ; resq: reserve quadword (64 bits) .tv_sec: resq 1 .tv_nsec: resq 1 endstruc
Now we get to exercise nasm's syntax for defining an instance of a struct in the data section:
timeout: istruc timeval at timeval.tv_sec, dq 0 at timeval.tv_nsec, dq 100000 iend
Then we're all set for adding the sleep call, right before .read_more:, so we can read any keypresses that happened during the sleep:
mov rax, SYS_select mov rdi, 0 ; nfds mov rsi, 0 ; readfds mov rdx, 0 ; writefds mov rcx, 0 ; errorfds mov r8, timeout ; timeout syscall
Your code should now approximate part4.asm.
And playing the game should look kind of like this:
Next lesson: TBA