AFFECTIVESILICON

Computer Spacegames

Revision 1.0 2024-07-05

In the Eighties, Usborne published some books aimed at young aspiring programmers. Once of these books was "Computer Spacegames" and it contained program listings in BASIC for the Spectrum, BBC micro, ZX81 etc. (PDFs of the books can be found here). While learning more about assembly language programming on Linux it seemed like a good project to try translating these simple BASIC games into assembly, and see what challenges they raised. Many of them are variants on the number guessing game, but with more story and mathematics. Others seem to be simulations.

Starship Takeoff

This is a straightforward variant of the number guessing game. To create a routine that generates small random numbers within a range, a byte from /dev/urandom is used a scaling value in the unit interval with some simple fixed-point mathematics.


; Starship Takeoff
; Adapted from "Computer Spacegames" by Daniel Isaaman and Jenny Tyler, Usborne Publishing
; https://usborne.com/gb/books/computer-and-coding-books
; nasm -g -f elf64 starship_takeoff.asm
; ld -o starship_takeoff starship_takeoff.o

section .data

	    ; various text strings used in the game
	    text1		        db	'STARSHIP TAKE-OFF', 0x0a
	    text1Len		    equ	$-text1
	    text2			    db	'GRAVITY= '	
	    text2Len		    equ	$-text2
	    text2_s		        times 10 db 0
	    text3			    db	'TYPE IN FORCE', 0x0a
	    text3Len		    equ	$-text3
	    text4			    db	'TOO HIGH'
	    text4Len		    equ	$-text4
	    text5			    db	'TOO LOW'
	    text5Len		    equ	$-text5
	    text6			    db	', TRY AGAIN', 0x0a
	    text6Len		    equ	$-text6
	    text7			    db	0x0a, 'YOU FAILED -', 0x0a, 'THE ALIENS GOT YOU', 0x0a
	    text7Len		    equ	$-text7
	    text8			    db	'GOOD TAKE OFF', 0x0a
	    text8Len		    equ	$-text8

	    ; a scratch buffer for number to string conversion
	    scratch		        times 10 db 0
	    scratchLen		    equ $-scratch
	    scratchend		    db 0

	    ; input buffer for player input
	    inbuf 			    times 10 db 0
	    inbufLen	        equ $-inbuf
	    null_char	        db 0

	    ; ANSI code to clear the screen
        cls_code            db      0x1b, '[2J', 0x1b, '[H'
        clsLen              equ     $-cls_code

	    ; take random numbers from /dev/urandom
	    errMsgRand         	db      'Could not open /dev/urandom', 0x0a
	    errMsgRandLen      	equ     $-errMsgRand

	    randSrc        	    db      '/dev/urandom', 0x0
	    randNum        	    db      0

section .bss

	    ; in-game variables
	    G_var			    resb	2
	    W_var			    resb	2
	    R_var			    resb	2
	    C_var			    resb	1

section .text

	    global _start

_start:
	    ; try to follow the program listing as closely as possible
_10:	; CLS
        mov rsi, cls_code
        mov rdx, clsLen
        call write_out        
_20:	; PRINT "STARSHIP TAKE-OFF"
        mov rsi, text1  
        mov rdx, text1Len 
	    call write_out
_30:	; LET G=INT(RND*20+1)
	    mov rax, 20
	    mov rcx, 1
	    call rand_func
	    mov word [G_var], dx
_40:	; LET W=INT(RND*40+1)
	    mov rax, 40
	    mov rcx, 1
	    call rand_func
	    mov word [W_var], dx
_50:	; LET R=G*W
	    mov bx, dx
	    movzx rax, word [G_var]
	    imul bx
	    mov word [R_var], ax
_60:	; PRINT "GRAVITY= ";G
	    movzx rax, word [G_var]
	    mov r9, text2
	    mov r10, text2Len
	    mov r11, text2_s
	    call display_line
_70:	; PRINT "TYPE IN FORCE"
	    mov rsi, text3
	    mov rdx, text3Len
	    call write_out
_80:	; FOR C=1 TO 10
	    mov byte [C_var], 1
_90:	; INPUT F
	    call read_string
	    mov rsi, inbuf 
	    call string_to_num
_100:	; IF F>R THEN PRINT "TOO HIGH";
	    movzx rbx, word [R_var]
	    cmp rbx, rcx
	    jg too_high
_110:	; IF F<R THEN PRINT "TOO LOW";
	    jl too_low
_120:	; IF F=R THEN GOTO 190
	    je _190
_130:	; IF C<>10 THEN PRINT ", TRY AGAIN"
	    cmp byte [C_var], 10
	    je _150
	    mov rsi, text6
	    mov rdx, text6Len
	    call write_out
_140:	; NEXT C
	    add byte [C_var], 1
	    jmp _90
_150:	; PRINT
_160:	; PRINT "YOU FAILED -"
_170:	; PRINT "THE ALIENS GOT YOU"
	    mov rsi, text7
	    mov rdx, text7Len
	    call write_out
_180:	; STOP
	    jmp exit
_190:	; print "GOOD TAKE OFF"
	    mov rsi, text8
	    mov rdx, text8Len
	    call write_out
exit:
	    mov rax, 0x3c       
	    mov rdi, 0
	    syscall

;;;;;;;;;;;;;;;;;;;;;;

; display text for too high and too low
too_low:
	    mov rsi, text4
	    mov rdx, text4Len
	    call write_out
	    jmp _130

too_high:
	    mov rsi, text5
	    mov rdx, text5Len
	    call write_out
	    jmp _130

	    ; random number function. Pulls a byte from /dev/urandom which is used as a random number
	    ; >= 0 and < 1. Pass in a multiplier to this in rax, and an offset to add in rcx.
rand_func:
	    ; on entry, rax is the multiplier, rcx is the offset.
	    ; in practice it's ax and cx, as it will only work for small numbers
	    push rcx
	    push rax

	    ; open the source of randomness
	    mov rax, 2              ; 'open'
	    mov rdi, randSrc        ; pointer to filename
	    mov rsi, 0              ; flags: 0 is O_RDONLY on my system
	    mov rdx, 0              
	    syscall
    
	    cmp rax, -2             ; file not found           
	    je open_error
	    cmp rax, -13            ; permission denied
	    je open_error

	    mov rbx, rax            ; save the file descriptor

	    ; read a byte
	    mov rax, 0              ; 'read'
	    mov rdi, rbx            ; file descriptor
	    mov rsi, randNum        ; memory location to read to
	    mov rdx, 1              ; read 1 byte
	    push rbx               
	    syscall
	    pop rbx

	    ; close it
	    mov rax, 3              ; 'close'
	    mov rdi, rbx            ; file descriptor
	    syscall

	    ; some fixed-point math. 
	    ; say we have 8 bits of fractional part, and that is the
	    ; random number obtained above, so 0<=rand<1
	    movzx rbx, byte [randNum]   
	    pop rax
	    ; multiply the number in rax by 256 to maintain the fixed-point math.
	    shl rax, 8
	    imul bx
	    ; because the result is in dx:ax, dx already contains the integer portion of the random number.
	    ; so don't have to divide by 256*256.
	    ; add the offset in rcx
	    pop rcx
	    add rdx, rcx
	    ret

open_error:
	    ; display a simple message and exit if could not open /dev/urandom
	    mov rsi, errMsgRand
	    mov rdx, errMsgRandLen
	    call write_out
	    jmp exit

	    ; this is to display the random number for the gravity
display_line:
	    ; 1. Convert number to string in scratch buffer
	    mov r8, 10		    	; we divide repeatedly by 10 to convert number to string
	    mov rdi, scratchend		; start from the end of the scratch buffer and work back
	    mov rcx, 0		    	; this will contain the final number of chars
itoa_inner:
	    dec rdi			    	; going backwards in memory
	    mov rdx, 0		    	; set up the division: rax already set coming into procedure
	    div r8			    	; divide by ten
	    add rdx, 0x30	    	; offset the remainder of the division to get the required ascii char
	    mov [rdi], dl			; write the ascii char to the scratch buffer
	    inc rcx			    	; keep track of the number of chars produced
	    cmp rcx, scratchLen		; try not to overfeed the buffer
	    je itoa_done			; break out if we reach the end of the buffer 
	    cmp rax, 0		    	; otherwise keep dividing until nothing left 
	    jne itoa_inner
itoa_done:
	    ; 2. Copy contents of scratch buffer into correct place in output string
	    ; rdi now points to beginning of char string and rcx is the number of chars
	    ; copy number into display buffer
	    mov rsi, rdi
	    mov rdi, r11            ; r11 is set coming into procedure, points to where in memory the number string should go
	    ; rcx already set from above
	    mov r8, rcx;		    ; preserve number of chars in number string 
	    rep movsb		        ; copy the number string to the output buffer
	    mov byte [rdi], 0x0a	; and put a newline on the end of it
show_num:
	    ; 3. Write the complete final string to stdout
	    mov rsi, r9		    	; pointer to final char buffer, r9 is set coming into procedure
	    ; calculate number of chars to display
	    mov rdx, r10 			; length of the preamble, r10 set coming into procedure
	    add rdx, r8		    	; plus length of the number string we just made
	    inc rdx			    	; plus one for newline char
	    mov rax, 1		    	; write
	    mov rdi, 1		    	; to stdout
	    syscall             	; execute
	    ret                 	; done


read_string:
	    ; player is going to enter something in the terminal
	    mov rcx, 0		        ; count number of chars entered
get_char:
	    ; read a char into the buffer
	    mov rax, 0		        ; read
	    mov rdi, 0		        ; from stdin
	    mov rdx, 1		        ; 1 char
	    mov rsi, inbuf		    ; calculate the current offset into input buffer
	    add rsi, rcx		    ; fill it up one char at a time until newline entered
	    push rsi		        ; preserve the pointer
	    push rcx		        ; and the counter
	    syscall
	    pop rcx			        ; restore
	    pop rsi
	    cmp rax, 0		        ; check for nothing read
	    je exit;		        ; for now just quit
	    inc rcx			        ; increment counter
	    movzx rax, byte [rsi]	; check for newline entered
	    cmp rax, 0x0a
	    je done_read		    ; break out of loop when user hits return 
	    cmp rcx, inbufLen
	    jge exit;		        ; let's not read beyond the end of the buffer
	    jmp get_char		    ; continue
done_read:
	    mov byte [rsi], 0
	    ret


string_to_num:
	    mov rcx, 0			        ; rcx will be the final number
atoi_loop:
	    movzx rbx, byte [rsi]       ; get the char pointed to by rsi
	    cmp rbx, 0x30               ; Check if char is below '0' 
	    jl exit
	    cmp rbx, 0x39               ; Check if char is above '9'
	    jg exit
	    sub rbx, 0x30               ; adjust to actual number by subtracting ASCII offset to 0
	    add rcx, rbx                ; accumulate number in rcx
	    movzx rbx, byte [rsi+1]     ; check the next char to see if the string continues
	    cmp rbx, 0                  ; string should be null-terminated
	    je done_string			    ; if it's null we're done converting
	    imul rcx, 10                ; multiply rcx by ten
	    inc rsi                     ; increment pointer to get next char when we loop
	    jmp atoi_loop
done_string:
	    ; rcx is the number
	    ret

	    ; write a string to stdout
write_out:
        mov rax, 1          
        mov rdi, 1         
	    syscall
	    ret
				
				

Intergalactic Games

In this game there are two numbers to guess. Some mathematics are required which makes this a more difficult implementation, an arctangent and a square root. It seems possible to do both of these with the old maths coprocessor instructions, but to be a little more modern the SSE2 instructions were used. That took care of the square root, but faced with having to approximate the arctangent with a series, the easier route seemed the best and a lookup table was generated. Unfortunately for the game, the way the mathematics works out, the angle is almost always in the mid eighties and the speed is almost always close to 3000.

Because of the SIMD (SSE2) instructions, data alignment becomes critically important. Also after reading a bit more about best practices, this code begins using function prologues and epilogues, to be a little less amateur.


; Intergalactic Games
; Adapted from "Computer Spacegames" by Daniel Isaaman and Jenny Tyler, Usborne Publishing
; https://usborne.com/gb/books/computer-and-coding-books
; nasm -g -f elf64 intergalactic_games.asm
; ld -o intergalactic_games intergalactic_games.o

section .data

	    text1		    db	    'INTERGALACTIC GAMES', 0x0a
	    text1Len	    equ	$-text1
	    text2		    db	    'YOU MUST LAUNCH A SATELLITE', 0x0a
	    text2Len	    equ	$-text2
	    text3		    db	    'TO A HEIGHT OF '
	    text3Len	    equ	$-text3
	    text3_s	        times 10 db 0
	    text4		    db	    'ENTER ANGLE (0-90)', 0x0a
	    text4Len	    equ	$-text4
	    text5		    db	    'ENTER SPEED (0-40000)', 0x0a
	    text5Len	    equ	$-text5
	    text6		    db	    'TOO SHALLOW', 0x0a
	    text6Len	    equ	$-text6
	    text7		    db	    'TOO STEEP', 0x0a
	    text7Len	    equ	$-text7
	    text8		    db	    'TOO SLOW', 0x0a
	    text8Len	    equ	$-text8
	    text9		    db	    'TOO FAST', 0x0a
	    text9Len	    equ	$-text9
	    text10		    db	    "YOU'VE FAILED", 0x0a, "YOU'RE FIRED", 0x0a
	    text10Len	    equ	$-text10
	    text11		    db	    "YOU'VE DONE IT", 0x0a, 'NCTV WINS-THANKS TO YOU', 0x0a
	    text11Len	    equ	$-text11
	
	    ; a scratch buffer for number to string conversion
    	scratch		    times 10 db 0
	    scratchLen		equ $-scratch
	    scratchend		db 0

	    ; input buffer for player input
	    inbuf 			times 10 db 0
	    inbufLen	    equ $-inbuf
	    null_char	    db 0

        ; precomputed table of 256 (very) approximate atan values for byte indexing, e.g. in perl:
        ; use Math::Trig;
        ; print join(", ", (map { sprintf("%d", rad2deg(atan2($_*100/256,3))) } (0..255))), "\n";
        atan_table		db	0, 7, 14, 21, 27, 33, 37, 42, 46, 49, 52, 55, 57, 59, 61, 62, 64, 65, 66, 67, 68, 69, 70, 71, 72, 72, 73, 74, 74, 75, 75, 76, 76, 76, 77, 77, 77, 78, 78, 78, 79, 79, 79, 79, 80, 80, 80, 80, 80, 81, 81, 81, 81, 81, 81, 82, 82, 82, 82, 82, 82, 82, 82, 83, 83, 83, 83, 83, 83, 83, 83, 83, 83, 83, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 84, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88

	    ; take random numbers from /dev/urandom
	    errMsgRand         	db      'Could not open /dev/urandom', 0x0a
	    errMsgRandLen      	equ     $-errMsgRand

	    randSrc         	db      '/dev/urandom', 0x0
	    randNum         	db      0

	    SSE     			db	'CPU with SSE2 support is required.', 0x0a
	    SSELen  			equ	$-SSE

    	V_multiplier        dq      3000.0
	    align 16	        ; must align this to 16 bytes for movapd later on
    	abs_mask            dq      0x7FFFFFFFFFFFFFFF

section .bss

	    H_var		        resw	1
	    G_var		        resb	1
	    A_var		        resq	1
	    A_abs		        resq	1
	    V_var		        resq	1
	    V_abs		        resq	1

section .text
    	global _start

_start:
        ; before proceeding, check for SSE2
        ; not sure what the chances are of finding an x86 64 cpu without
        ; SSE2 support, but it seems like a good idea to check anyway
        mov rax, 1
        cpuid
        test edx, 1<<26
        jnz _10
        mov rsi, SSE
        mov rdx, SSELen
        call write_out
        jmp exit
        ; no CLS in this one
_10:	; PRINT "INTERGALACTIC GAMES"
        mov rsi, text1
        mov rdx, text1Len
        call write_out
_20:	; LET H=INT(RND*100+1)
        mov rax, 100
        mov rcx, 1
        call rand_func
        mov word [H_var], dx
_30:	; PRINT "YOU MUST LAUNCH A SATELLITE"
        mov rsi, text2
        mov rdx, text2Len
        call write_out
_40:	; PRINT "TO A HEIGHT OF ";H	; listing doesn't mention any units
        movzx rax, word [H_var]              
        mov r9, text3
        mov r10, text3Len
        mov r11, text3_s
        call display_line
_50:	; FOR G=1 TO 8
        mov byte [G_var], 1
_60:	; PRINT "ENTER ANGLE (0-90)"
        mov rsi, text4
        mov rdx, text4Len
        call write_out
_70:	; INPUT A
        call read_string
        mov rsi, inbuf 
        call string_to_num
        mov [A_var], rcx
_80:	; PRINT "ENTER SPEED (0-40000) ; looks like a typo on this line, should have '"'
        mov rsi, text5
        mov rdx, text5Len
        call write_out
_90:	; INPUT V
        call read_string
        mov rsi, inbuf 
        call string_to_num
        mov [V_var], rcx
_100:	; LET A=A-ATN(H/3)*180/3.14159
        ; Here we do (H/3) * scaling factor of 7.5 for lookup table
        ; Which is (H*15)/6
        ; Since H is constant in the loop we don't really have to do all this every time, can put it before the loop. But here we are
        mov ax, word [H_var]
        mov bx, 15
        mul bx				
        mov bx, 6
        div bx
        ; al contains offset into the lookup table
        ; perform lookup to get (very) approximate atan value
        mov rbx, atan_table
        xlatb
        ; atan table already converted to degrees
        sub [A_var], rax ; A=A-ATN(H/3)*rad2deg
_110:	; LET V=V-3000*SQR(H+1/H)
        movzx rax, word [H_var]	
        ; use SSE2 instructions for this one
        cvtsi2sd xmm1, rax	        ; xmm1 = H
        inc rax                     ; rax = H+1
        cvtsi2sd xmm0, rax          ; xmm0 = H+1
        divsd xmm0, xmm1            ; xmm0 = H+1/H
        sqrtsd xmm1, xmm0           ; xmm1 = SQR(H+1/H)
        movsd xmm0, [V_multiplier]  ; xmm0 = 3000
        mulsd xmm0, xmm1            ; xmm0 = 3000*SQR(H+1/H)
        mov rax, [V_var]            ; rax = V
        cvtsi2sd xmm1, rax          ; xmm1 = V
        subsd xmm1, xmm0            ; xmm1 = V-3000*SQR(H+1/H)
        cvtsd2si rax, xmm1      
        mov [V_var], rax	    ; V=V-3000*SQR(H+1/H) 
_120:	; IF ABS(A)<2 AND ABS(V)<100 THEN GOTO 200 ; typo, should be GOTO 210
        ; do ABS(V)
        movapd xmm0, [abs_mask]     ; this is apparently the way to do abs in SSE2
        andpd xmm1, xmm0            ; clear sign bit. xmm1 = ABS(V-3000*SQRT(H+1/H))
        cvtsd2si rax, xmm1      
        mov [V_abs], rax
        ; do ABS(A)
        mov rax, [A_var]
        ; this bit manipulation is apparently the way to do abs on a general register
        cqo
        xor rax, rdx
        sub rax, rdx
        mov [A_abs], rax
        ; do "ABS(A)<2 AND ABS(v)<100"
        cmp qword [A_abs], 2
        jge _130
        cmp qword [V_abs], 100
        jge _130
        ; do "GOTO 200" (actually 210)
        jmp _210
_130:	; IF A<-2 THEN PRINT "TOO SHALLOW"	; assume that <=, >= etc. is meant in lines 130-160 
        cmp qword [A_var], -2
        jg _140
        mov rsi, text6
        mov rdx, text6Len
        call write_out
_140:	; IF A>2 THEN PRINT "TOO STEEP"	
        cmp qword [A_var], 2
        jl _150
        mov rsi, text7
        mov rdx, text7Len
        call write_out
_150:	; IF V<-100 THEN PRINT "TOO SLOW"
        cmp qword [V_var], -100
        jg _160
        mov rsi, text8
        mov rdx, text8Len
        call write_out
_160:	; IF V>100 THEN PRINT "TOO FAST"
        cmp qword [V_var], 100
        jl _170
        mov rsi, text9
        mov rdx, text9Len
        call write_out
_170:	; NEXT G
        cmp byte [G_var], 8
        je _180
        add byte [G_var], 1
        jmp _60
_180:	; PRINT "YOU'VE FAILED"
_190:	; PRINT "YOU'RE FIRED"
        mov rsi, text10
        mov rdx, text10Len
        call write_out
_200:	; STOP
        jmp exit
_210:	; PRINT "YOU'VE DONE IT"
_220:	; PRINT "NCTV WINS-THANKS TO YOU"
        mov rsi, text11
        mov rdx, text11Len
        call write_out
_230:	; STOP
        exit:
        mov rax, 0x3c       
        mov rdi, 0
        syscall


	    ; random number function. Pulls a byte from /dev/urandom which is used as a random number
	    ; >= 0 and < 1. Pass in a multiplier to this in rax, and an offset to add in rcx.
rand_func:
	    push rbp
	    mov rbp, rsp

	    ; on entry, rax is the multiplier, rcx is the offset.
	    ; in practice it's ax and cx, as it will only work for small numbers
	    push rcx
	    push rax

	    ; open the source of randomness
	    mov rax, 2              ; 'open'
	    mov rdi, randSrc        ; pointer to filename
	    mov rsi, 0              ; flags: 0 is O_RDONLY on my system
	    mov rdx, 0              
	    syscall
    
	    cmp rax, -2             ; file not found           
	    je open_error
	    cmp rax, -13            ; permission denied
	    je open_error

	    mov rbx, rax            ; save the file descriptor

	    ; read a byte
	    mov rax, 0              ; 'read'
    	mov rdi, rbx            ; file descriptor
    	mov rsi, randNum        ; memory location to read to
    	mov rdx, 1              ; read 1 byte
    	push rbx               
    	syscall
    	pop rbx

    	; close it
	    mov rax, 3              ; 'close'
    	mov rdi, rbx            ; file descriptor
	    syscall

	    ; some fixed-point math. 
	    ; say we have 8 bits of fractional part, and that is the
	    ; random number obtained above, so 0<=rand<1
	    movzx rbx, byte [randNum]   
	    pop rax
	    ; multiply the number in rax by 256 to maintain the fixed-point math.
	    shl rax, 8
	    imul bx
	    ; because the result is in dx:ax, dx already contains the integer portion of the random number.
	    ; so don't have to divide by 256*256.
	    ; add the offset in rcx
	    pop rcx
	    add rdx, rcx

	    pop rbp
	    ret


open_error:
        ; display a simple message and exit if could not open /dev/urandom
        mov rsi, errMsgRand
        mov rdx, errMsgRandLen
        call write_out
        jmp exit


        ; display an output string with a number on the end
display_line:
        push rbp
        mov rbp, rsp

        ; 1. Convert number to string in scratch buffer
        mov r8, 10		    	    ; we divide repeatedly by 10 to convert number to string
        mov rdi, scratchend		    ; start from the end of the scratch buffer and work back
        mov rcx, 0		    	    ; this will contain the final number of chars
itoa_inner:
        dec rdi			    	    ; going backwards in memory
        mov rdx, 0		    	    ; set up the division: rax already set coming into procedure
        div r8			    	    ; divide by ten
        add rdx, 0x30	    		; offset the remainder of the division to get the required ascii char
        mov [rdi], dl			    ; write the ascii char to the scratch buffer
        inc rcx			    	    ; keep track of the number of chars produced
        cmp rcx, scratchLen		    ; try not to overfeed the buffer
        je itoa_done			    ; break out if we reach the end of the buffer 
        cmp rax, 0		    	    ; otherwise keep dividing until nothing left 
        jne itoa_inner
itoa_done:
        ; 2. Copy contents of scratch buffer into correct place in output string
        ; rdi now points to beginning of char string and rcx is the number of chars
        ; copy number into display buffer
        mov rsi, rdi
        mov rdi, r11            	; r11 is set coming into procedure, points to where in memory the number string should go
        ; rcx already set from above
        mov r8, rcx;		    	; preserve number of chars in number string 
        rep movsb		            ; copy the number string to the output buffer
        mov byte [rdi], 0x0a		; and put a newline on the end of it
show_num:
        ; 3. Write the complete final string to stdout
        mov rsi, r9		    	    ; pointer to final char buffer, r9 is set coming into procedure
        ; calculate number of chars to display
        mov rdx, r10 			    ; length of the preamble, r10 set coming into procedure
        add rdx, r8		    	    ; plus length of the number string we just made
        inc rdx			    	    ; plus one for newline char
        mov rax, 1		    	    ; write
        mov rdi, 1		    	    ; to stdout
        syscall             		; execute

        pop rbp
        ret                 		; done


read_string:
	    push rbp
	    mov rbp, rsp

; player is going to enter something in the terminal
    	mov rcx, 0		            ; count number of chars entered
get_char:
	    ; read a char into the buffer
	    mov rax, 0		            ; read
	    mov rdi, 0		            ; from stdin
	    mov rdx, 1		            ; 1 char
	    mov rsi, inbuf		        ; calculate the current offset into input buffer
	    add rsi, rcx		        ; fill it up one char at a time until newline entered
	    push rsi		            ; preserve the pointer
	    push rcx		            ; and the counter
	    syscall
	    pop rcx			            ; restore
	    pop rsi
	    cmp rax, 0		            ; check for nothing read
	    je done_read		        ; for now just quit
	    inc rcx			            ; increment counter
	    movzx rax, byte [rsi]	    ; check for newline entered
	    cmp rax, 0x0a
	    je done_read		        ; break out of loop when user hits return 
	    cmp rcx, inbufLen
	    jge done_read		        ; let's not read beyond the end of the buffer
	    jmp get_char		        ; continue
done_read:
	    mov byte [rsi], 0

	    pop rbp
	    ret


string_to_num:
	    push rbp
	    mov rbp, rsp

	    mov rcx, 0			            ; rcx will be the final number
atoi_loop:
	    movzx rbx, byte [rsi]       	; get the char pointed to by rsi
	    cmp rbx, 0x30               	; Check if char is below '0' 
	    jl exit
	    cmp rbx, 0x39               	; Check if char is above '9'
	    jg exit
	    sub rbx, 0x30               	; adjust to actual number by subtracting ASCII offset to 0
	    add rcx, rbx                	; accumulate number in rcx
	    movzx rbx, byte [rsi+1]     	; check the next char to see if the string continues
	    cmp rbx, 0                  	; string should be null-terminated
	    je done_string			        ; if it's null we're done converting
	    imul rcx, 10                	; multiply rcx by ten
	    inc rsi                     	; increment pointer to get next char when we loop
	    jmp atoi_loop
done_string:
	    ; rcx is the number
	    pop rbp
	    ret

	    ; write a string to stdout
write_out:
        mov rax, 1          
        mov rdi, 1         
	    syscall
	    ret
				
				

Evil Alien

There are three numbers to guess in this game.


; Evil Alien
; Adapted from "Computer Spacegames" by Daniel Isaaman and Jenny Tyler, Usborne Publishing
; https://usborne.com/gb/books/computer-and-coding-books
; nasm -g -f elf64 evil_alien.asm
; ld -o evil_alien evil_alien.o

section .data

	    text1		        db	    'EVIL ALIEN', 0x0a
	    text1Len	        equ	$-text1
	    text2		        db	    'X POSITION (0 TO 9)?', 0x0a
	    text2Len	        equ	$-text2
	    text3		        db	    'Y POSITION (0 TO 9)?', 0x0a
	    text3Len	        equ	$-text3
	    text4		        db	    'DISTANCE (0 TO 9)?', 0x0a
	    text4Len	        equ	$-text4
	    text5		        db	    'SHOT WAS '
	    text5Len	        equ	$-text5
	    text6		        db	    'NORTH'
	    text6Len	        equ	$-text6
	    text7		        db	    'SOUTH'
	    text7Len	        equ	$-text7
	    text8		        db	    'EAST'
	    text8Len	        equ	$-text8
	    text9		        db	    'WEST'
	    text9Len	        equ	$-text9
	    text10		        db	    0x0a
	    text10Len	        equ	$-text10
	    text11		        db	    'TOO FAR', 0x0a
	    text11Len	        equ	$-text11
	    text12		        db	    'NOT FAR ENOUGH', 0x0a
	    text12Len	        equ	$-text12
	    text13		        db	    'YOUR TIME HAS RUN OUT!!', 0x0a
	    text13Len	        equ	$-text13
	    text14		        db	    '*BOOM* YOU GOT HIM!', 0x0a
	    text14Len	        equ	$-text14

        ; code to clear the screen
        cls_code            db      0x1b, '[2J', 0x1b, '[H'
        clsLen              equ     $-cls_code

    	; take random numbers from /dev/urandom
	    errMsgRand         	db      'Could not open /dev/urandom', 0x0a
	    errMsgRandLen      	equ     $-errMsgRand

	    randSrc         	db      '/dev/urandom', 0x0
	    randNum         	db      0

	    ; input buffer for player input
	    inbuf    			times 10 db 0
	    inbufLen	    	equ $-inbuf
	    null_char	    	db 0

section .bss

	    S_var		        resb	1
	    G_var		        resb	1
	    X_var		        resb	1
	    Y_var		        resb	1
	    D_var		        resb	1
	    I_var		        resb	1
	    X1_var		        resb	1
	    Y1_var		        resb	1
	    D1_var		        resb	1

section .text

    	global _start

_start:
_5:	    ; CLS
        mov rsi, cls_code
        mov rdx, clsLen
        call write_out
_10:	; PRINT "EVIL ALIEN"
	    mov rsi, text1
	    mov rdx, text1Len
	    call write_out
_20:	; LET S=10
	    mov byte [S_var], 10
_30:	; LET G=4
    	mov byte [G_var], 4
_40:	; LET X=INT(RND*S)
	    movzx rax, byte [S_var]
	    xor ecx, ecx
	    call rand_func
	    mov byte [X_var], dl
_50:	; LET Y=INT(RND*S)
	    movzx rax, byte [S_var]
	    xor ecx, ecx
	    call rand_func
	    mov byte [Y_var], dl
_60: 	; LET D=INT(RND*S)
	    movzx rax, byte [S_var]
	    xor ecx, ecx
	    call rand_func
	    mov byte [D_var], dl
_70:	; FOR I=1 TO G
    	mov byte [I_var], 1
_80:	; PRINT "X POSITION (0 TO 9)?" 
	    mov rsi, text2
	    mov rdx, text2Len
	    call write_out
_85:	; INPUT X1
	    call read_string
	    mov rsi, inbuf 
	    call string_to_num
	    mov [X1_var], cl
_90:	; PRINT "Y POSITION (0 TO 9)?"
	    mov rsi, text3
	    mov rdx, text3Len
	    call write_out
_100:	; INPUT Y1
	    call read_string
	    mov rsi, inbuf 
	    call string_to_num
	    mov [Y1_var], cl
_110:	; PRINT "DISTANCE (0 TO 9)?"
	    mov rsi, text4
	    mov rdx, text4Len
	    call write_out
_120:	; INPUT D1
	    call read_string
	    mov rsi, inbuf 
	    call string_to_num
	    mov [D1_var], cl
_130:	; IF X=X1 AND Y=Y1 AND D=D1 THEN GOTO 300
	    mov al, byte [X_var]
	    cmp al, [X1_var]
	    jne _140
	    mov al, byte [Y_var]
	    cmp al, [Y1_var]
	    jne _140
	    mov al, byte [D_var]
	    cmp al, [D1_var]
	    jne _140
	    jmp _300
_140:	; PRINT "SHOT WAS ";
	    mov rsi, text5
	    mov rdx, text5Len
	    call write_out
_150:	; IF Y1>Y THEN PRINT "NORTH";
	    mov al, [Y1_var]
	    cmp al, [Y_var]
	    jle _160
	    mov rsi, text6
	    mov rdx, text6Len
	    call write_out
_160:	; IF Y1<Y THEN PRINT "SOUTH";
	    mov al, [Y1_var]
	    cmp al, [Y_var]
	    jge _170
	    mov rsi, text7
	    mov rdx, text7Len
	    call write_out
_170:	; IF X1>X THEN PRINT "EAST";
	    mov al, [X1_var]
	    cmp al, [X_var]
	    jle _180
	    mov rsi, text8
	    mov rdx, text8Len
	    call write_out
_180:	; IF X1<X THEN PRINT "WEST";
	    mov al, [X1_var]
	    cmp al, [X_var]
	    jge _190
	    mov rsi, text9
	    mov rdx, text9Len
	    call write_out
_190:	; PRINT
	    mov rsi, text10
	    mov rdx, text10Len
	    call write_out
_200:	; IF D1>D THEN PRINT "TOO FAR"
	    mov al, [D1_var]
	    cmp al, [D_var]
	    jle _210
	    mov rsi, text11
	    mov rdx, text11Len
	    call write_out
_210:	; IF D1<D THEN PRINT "NOT FAR ENOUGH"
	    mov al, [D1_var]
	    cmp al, [D_var]
	    jge _220
	    mov rsi, text12
	    mov rdx, text12Len
	    call write_out
_220:	; NEXT I
	    mov al, byte [G_var]
	    cmp byte [I_var], al
	    je _230
	    add byte [I_var], 1
	    jmp _80
_230:	; PRINT "YOUR TIME HAS RUN OUT!!"
	    mov rsi, text13
	    mov rdx, text13Len
	    call write_out
_240:	; STOP
    	jmp exit
_300:	; PRINT "*BOOM* YOU GOT HIM!"
	    mov rsi, text14
	    mov rdx, text14Len
	    call write_out
_310:	; STOP
exit:
	    mov rax, 0x3c       
	    mov rdi, 0
	    syscall



	    ; write a string to stdout
write_out:
        mov rax, 1          
        mov rdi, 1         
	    syscall
	    ret


 
	    ; random number function. Pulls a byte from /dev/urandom which is used as a random number
	    ; >= 0 and < 1. Pass in a multiplier to this in rax, and an offset to add in rcx.
rand_func:
	    push rbp
	    mov rbp, rsp

	    ; on entry, rax is the multiplier, rcx is the offset.
	    ; in practice it's ax and cx, as it will only work for small numbers
	    push rcx
	    push rax

	    ; open the source of randomness
	    mov rax, 2              ; 'open'
	    mov rdi, randSrc        ; pointer to filename
	    mov rsi, 0              ; flags: 0 is O_RDONLY on my system
	    mov rdx, 0              
	    syscall
        
	    cmp rax, -2             ; file not found           
	    je open_error
	    cmp rax, -13            ; permission denied
	    je open_error

	    mov rbx, rax            ; save the file descriptor

	    ; read a byte
	    mov rax, 0              ; 'read'
	    mov rdi, rbx            ; file descriptor
	    mov rsi, randNum        ; memory location to read to
	    mov rdx, 1              ; read 1 byte
	    push rbx               
	    syscall
	    pop rbx

	    ; close it
	    mov rax, 3              ; 'close'
	    mov rdi, rbx            ; file descriptor
	    syscall

	    ; some fixed-point math. 
	    ; say we have 8 bits of fractional part, and that is the
	    ; random number obtained above, so 0<=rand<1
	    movzx rbx, byte [randNum]   
	    pop rax
	    ; multiply the number in rax by 256 to maintain the fixed-point math.
	    shl rax, 8
	    imul bx
	    ; because the result is in dx:ax, dx already contains the integer portion of the random number.
	    ; so don't have to divide by 256*256.
	    ; add the offset in rcx
	    pop rcx
	    add rdx, rcx

	    pop rbp
	    ret

open_error:
	    ; display a simple message and exit if could not open /dev/urandom
	    mov rsi, errMsgRand
	    mov rdx, errMsgRandLen
	    call write_out
	    jmp exit


read_string:
	    push rbp
	    mov rbp, rsp

; player is going to enter something in the terminal
    	mov rcx, 0		; count number of chars entered
get_char:
	    ; read a char into the buffer
	    mov rax, 0		; read
	    mov rdi, 0		; from stdin
	    mov rdx, 1		; 1 char
	    mov rsi, inbuf		; calculate the current offset into input buffer
	    add rsi, rcx		; fill it up one char at a time until newline entered
	    push rsi		; preserve the pointer
	    push rcx		; and the counter
	    syscall
	    pop rcx			; restore
	    pop rsi
	    cmp rax, 0		; check for nothing read
	    je done_read		; for now just quit
	    inc rcx			; increment counter
	    movzx rax, byte [rsi]	; check for newline entered
	    cmp rax, 0x0a
	    je done_read		; break out of loop when user hits return 
	    cmp rcx, inbufLen
	    jge done_read		; let's not read beyond the end of the buffer
	    jmp get_char		; continue
done_read:
	    mov byte [rsi], 0

	    pop rbp
	    ret


string_to_num:
	    push rbp
	    mov rbp, rsp

	    mov rcx, 0			; rcx will be the final number
atoi_loop:
	    movzx rbx, byte [rsi]       	; get the char pointed to by rsi
	    cmp rbx, 0x30               	; Check if char is below '0' 
	    jl exit
	    cmp rbx, 0x39               	; Check if char is above '9'
	    jg exit
	    sub rbx, 0x30               	; adjust to actual number by subtracting ASCII offset to 0
	    add rcx, rbx                	; accumulate number in rcx
	    movzx rbx, byte [rsi+1]     	; check the next char to see if the string continues
	    cmp rbx, 0                  	; string should be null-terminated
	    je done_string			; if it's null we're done converting
	    imul rcx, 10                	; multiply rcx by ten
	    inc rsi                     	; increment pointer to get next char when we loop
	    jmp atoi_loop
done_string:
	    ; rcx is the number
	    pop rbp
	    ret

				
				

Moonlander

This is a surprisingly intense game, containing a simple physics simulation. The only implementation consideration was that the velocity can quickly go negative, requiring the display function to be able to handle negative numbers. This was straightforwardly achieved by checking if the number is below zero to determine whether to write a minus sign and then displaying the absolute value of the number as usual.


; Moonlander
; Adapted from "Computer Spacegames" by Daniel Isaaman and Jenny Tyler, Usborne Publishing
; nasm -g -f elf64 moonlander.asm
; ld -o moonlander moonlander.o

section .data

        text1			    db		'MOONLANDER', 0x0a
        text1Len		    equ		$-text1
        text2			    db		'TIME '
        text2Len		    equ		$-text2
        text2_s			    times 10 db 0
        text3			    db		'HEIGHT '
        text3Len		    equ		$-text3
        text3_s			    times 10 db 0
        text4			    db		'VEL. '
        text4Len		    equ		$-text4
        text4_s			    times 10 db 0
        text5			    db		'FUEL '
        text5Len		    equ		$-text5
        text5_s			    times 10 db 0
        text6			    db		'BURN? (0-30)', 0x0a
        text6Len		    equ		$-text6
        text7			    db		'YOU CRASHED-ALL DEAD', 0x0a
        text7Len		    equ		$-text7
        text8			    db		'OK-BUT SOME INJURIES', 0x0a
        text8Len		    equ		$-text8
        text9			    db		'GOOD LANDING.', 0x0a
        text9Len		    equ		$-text9

        ; code to clear the screen
        cls_code            db      0x1b, '[2J', 0x1b, '[H'
        cls_len             equ     $-cls_code

        ; input buffer for player input
        inbuf 			    times 10 db 0
        inbufLen	    	equ $-inbuf
        null_char	    	db 0

        ; a scratch buffer for number to string conversion
        scratch		    	times 10 db 0
        scratchLen		    equ $-scratch
        scratchend		    db 0

section .bss

	    T_var			    resq	1
	    H_var			    resq	1
	    V_var			    resq	1
	    F_var			    resq	1
	    B_var			    resq	1	
	    V1_var			    resq	1

section .text

	    global _start

_start:
_10:	; CLS
	    mov rsi, cls_code
        mov rdx, cls_len
        call write_out        
_20:	; PRINT "MOONLANDER"
	    mov rsi, text1
	    mov rdx, text1Len
	    call write_out
_30:	; LET T=0
	    mov qword [T_var], 0
_40:	; LET H=500
	    mov qword [H_var], 500
_50:	; LET V=50
	    mov qword [V_var], 50
_60:	; LET F=120
	    mov qword [F_var], 120
_70:	; PRINT "TIME";T,"HEIGHT";H
        mov rax, qword [T_var]              
        mov r9, text2
        mov r10, text2Len
        mov r11, text2_s
	    mov r12, 0x09                   ; "tab"
        call display_line
	    mov rax, qword [H_var]              
        mov r9, text3
        mov r10, text3Len
        mov r11, text3_s
	    mov r12, 0x0a
        call display_line
_80:	; PRINT "VEL.";V,"FUEL";F
        mov rax, qword [V_var]              
        mov r9, text4
        mov r10, text4Len
        mov r11, text4_s
	    mov r12, 0x09
        call display_line
	    mov rax, qword [F_var]              
        mov r9, text5
        mov r10, text5Len
        mov r11, text5_s
	    mov r12, 0x0a
        call display_line
_90:	; IF F=0 THEN GOTO 140
	    cmp qword [F_var], 0
	    je _140
_100:	; PRINT "BURN? (0-30)"
	    mov rsi, text6
	    mov rdx, text6Len
	    call write_out
_110:	; INPUT B
        call read_string
        mov rsi, inbuf 
        call string_to_num
        mov [B_var], rcx
_120:	; IF B<0 THEN LET B=0
	    cmp qword [B_var], 0
	    jge _130
	    mov qword [B_var], 0
_130:	; IF B>30 THEN LET B=30
	    cmp qword [B_var], 30
	    jle _140
	    mov qword [B_var], 30
_140:	; IF B>F THEN LET B=F
	    mov rax, [F_var]
	    cmp qword [B_var], rax
	    jle _150
	    mov [B_var], rax
_150:	; LET V1=V-B+5
	    mov rax, [V_var]
	    sub rax, [B_var]
	    add rax, 5
	    mov qword [V1_var], rax
_160:	; LET F=F-B
	    mov rax, qword [B_var]
	sub [F_var], rax
_170:	; IF (V1+V)/2>H THEN GOTO 220
	    mov rax, [V1_var]
	    add rax, [V_var]
	    sar rax, 1
	    cmp rax, qword [H_var]
	    jg _220
_180:	; LET H=H-(V1+V)/2
	    mov rax, qword [V1_var]
	    add rax, qword [V_var]
	    sar rax, 1
	    sub qword [H_var], rax
_190:	; LET T=T+1
	    add qword [T_var], 1
_200:	; LET V=V1
	    mov rax, [V1_var]
	    mov [V_var], rax
_210:	; GOTO 70
	    jmp _70
_220:	; LET V1=V+(5-B)*H/V
	    mov rax, 5
	    sub rax, qword [B_var]
	    imul qword [H_var]
	    idiv qword [V_var]
	    add rax, qword [V_var]
	    mov [V1_var], rax
_230:	; IF V1>5 THEN PRINT "YOU CRASHED-ALL DEAD"
	    cmp qword [V1_var], 5
	    jle _240
	    mov rsi, text7
	    mov rdx, text7Len
	    call write_out
	    jmp _260
_240:	; IF V1>1 AND V1<=5 THEN PRINT "OK-BUT SOME INJURIES"
	    cmp qword [V1_var], 1
	    jle _250
	    mov rsi, text8
	    mov rdx, text8Len
	    call write_out
	    jmp _260
_250:	; IF V1<=1 THEN PRINT "GOOD LANDING."
	    mov rsi, text9
	    mov rdx, text9Len
	    call write_out
_260:	; STOP
exit:
	    mov rax, 0x3c       
	    mov rdi, 0
	    syscall


        ; display an output string with a number on the end
        ; this version can display negative numbers, and caller can specify how 
        ; to end the line (in this case, a tab or a newline)
display_line:
        push rbp
        mov rbp, rsp

	    ; 0. Figure out if the number in rax is negative and determine sign appropriately
	    xor r13, r13
	    mov r13b, '+'
	    cmp rax, 0
	    jge itoa_setup
	    ; is negative, setup sign
	    mov r13b, '-'
	    ; do abs
	    cqo
	    xor rax, rdx
	    sub rax, rdx
itoa_setup:
        ; 1. Convert number to string in scratch buffer
        mov r8, 10		    	    ; we divide repeatedly by 10 to convert number to string
        mov rdi, scratchend		    ; start from the end of the scratch buffer and work back
        mov rcx, 0		    	    ; this will contain the final number of chars
itoa_inner:
        dec rdi			    	    ; going backwards in memory
        mov rdx, 0		    	    ; set up the division: rax already set coming into procedure
        div r8			    	    ; divide by ten
        add rdx, 0x30	    		; offset the remainder of the division to get the required ascii char
        mov [rdi], dl			    ; write the ascii char to the scratch buffer
        inc rcx			    	    ; keep track of the number of chars produced
        cmp rcx, scratchLen		    ; try not to overfeed the buffer
        je itoa_done			    ; break out if we reach the end of the buffer 
        cmp rax, 0		    	    ; otherwise keep dividing until nothing left 
        jne itoa_inner
itoa_done:
        ; 2. Copy contents of scratch buffer into correct place in output string
        ; rdi now points to beginning of char string and rcx is the number of chars
        ; copy number into display buffer
        mov rsi, rdi
        mov rdi, r11            	; r11 is set coming into procedure, points to where in memory the number string should go
	    mov r8, rcx
	    cmp r13b, '+'
	    je past_minus
	    mov byte [rdi], r13b		; sign byte
	    inc rdi
	    inc r8
past_minus:
        ; rcx already set from above
        rep movsb		            ; copy the number string to the output buffer
        mov byte [rdi], r12b		; and put whatever's in r12b at the end of it
show_num:
        ; 3. Write the complete final string to stdout
        mov rsi, r9		    	    ; pointer to final char buffer, r9 is set coming into procedure
        ; calculate number of chars to display
        mov rdx, r10 			    ; length of the preamble, r10 set coming into procedure
        add rdx, r8		    	    ; plus length of the number string we just made
        inc rdx			    	    ; plus one for end char
        mov rax, 1		    	    ; write
        mov rdi, 1		    	    ; to stdout
        syscall             		; execute

        pop rbp
        ret                 		; done


read_string:
	    push rbp
	    mov rbp, rsp

; player is going to enter something in the terminal
    	mov rcx, 0		            ; count number of chars entered
get_char:
	    ; read a char into the buffer
	    mov rax, 0		            ; read
	    mov rdi, 0		            ; from stdin
	    mov rdx, 1		            ; 1 char
	    mov rsi, inbuf		        ; calculate the current offset into input buffer
	    add rsi, rcx		        ; fill it up one char at a time until newline entered
	    push rsi		            ; preserve the pointer
	    push rcx		            ; and the counter
	    syscall
	    pop rcx			            ; restore
	    pop rsi
	    cmp rax, 0		            ; check for nothing read
	    je done_read		        ; for now just quit
	    inc rcx			            ; increment counter
	    movzx rax, byte [rsi]	    ; check for newline entered
	    cmp rax, 0x0a
	    je done_read		        ; break out of loop when user hits return 
	    cmp rcx, inbufLen
	    jge done_read		        ; let's not read beyond the end of the buffer
	    jmp get_char		        ; continue
done_read:
	    mov byte [rsi], 0

	    pop rbp
	    ret


string_to_num:
	    push rbp
	    mov rbp, rsp

	    mov rcx, 0			            ; rcx will be the final number
atoi_loop:
	    movzx rbx, byte [rsi]       	; get the char pointed to by rsi
	    cmp rbx, 0x30               	; Check if char is below '0' 
	    jl exit
	    cmp rbx, 0x39               	; Check if char is above '9'
	    jg exit
	    sub rbx, 0x30               	; adjust to actual number by subtracting ASCII offset to 0
	    add rcx, rbx                	; accumulate number in rcx
	    movzx rbx, byte [rsi+1]     	; check the next char to see if the string continues
	    cmp rbx, 0                  	; string should be null-terminated
	    je done_string			        ; if it's null we're done converting
	    imul rcx, 10                	; multiply rcx by ten
	    inc rsi                     	; increment pointer to get next char when we loop
	    jmp atoi_loop
done_string:
	    ; rcx is the number
	    pop rbp
	    ret

	; write a string to stdout
write_out:
        mov rax, 1          
        mov rdi, 1         
	    syscall
	    ret 
				
				

Trip into the Future

The basis of this game is an adaptation of the time dilation equation, the prediction of Relativity that time would pass slower for a space traveller making a round trip from Earth and back at a high percentage of light speed. The player gets only one chance to input the correct speed and distance parameters to achieve a certain amount of time passing on Earth.

The new challenge here in terms of implementation was that the program has to be able to handle numbers containing a fractional part, both for input and display. It's no longer possible to work just with integers. This was done using a fixed point representation. On the input side, a number with integer and fractional parts can be represented as two integers, e.g. 12.34 can be represented by 1234 and 100 as the multiplier after detecting the location of the decimal point. For output, the same number can be represented as 12 for the integer part and 34 for the fractional part, separated by a decimal point when displayed.


; Trip into the Future
; Adapted from "Computer Spacegames" by Daniel Isaaman and Jenny Tyler, Usborne Publishing
; nasm -g -f elf64 trip_into_the_future.asm
; ld -o trip_into_the_future trip_into_the_future.o

section .data

        text1			        db		    'TRIP INTO THE FUTURE', 0x0a
        text1Len		        equ		    $-text1
        text2			        db		    'YOU WISH TO RETURN ',
        text2Len		        equ		    $-text2
        text2_s			        times 10 db 0
        text3			        db		    'YEARS INTO THE FUTURE.', 0x0a, 0x0a
        text3Len		        equ		    $-text3
        text4			        db		    'SPEED OF SHIP (0-1)', 0x0a
        text4Len		        equ		    $-text4
        text5			        db		    'DISTANCE OF TRIP', 0x0a
        text5Len		        equ		    $-text5
        text6			        db		    'YOU TOOK ',
        text6Len		        equ		    $-text6
        text6_s			        times 20 db 0
        text6_2			        db		    'YEARS', 0x0a,
        text6_2Len		        equ		    $-text6_2
        text7			        db		    'AND ARRIVED ',
        text7Len		        equ		    $-text7
        text7_s			        times 20 db 0
        text8			        db		    'IN THE FUTURE.', 0x0a
        text8Len		        equ		    $-text8
        text9			        db		    'YOU ARRIVED ON TIME', 0x0a
        text9Len		        equ		    $-text9
        text10			        db		    'NOT EVEN CLOSE', 0x0a
        text10Len		        equ		    $-text10
        text11			        db		    'YOU DIED ON THE WAY', 0x0a
        text11Len		        equ		    $-text11

        ; code to clear the screen
        cls_code                db      	0x1b, '[2J', 0x1b, '[H'
        cls_len                 equ     	$-cls_code

        ; a scratch buffer for number to string conversion
        scratch	    	        times 20 db 0
        scratchLen		        equ $-scratch
        scratchend		        db 0

        ; input buffer for player input
        inbuf 			        times 10 db 0
        inbuf_len	    	    equ $-inbuf
        null_char	    	    db 0

        ; take random numbers from /dev/urandom
        errMsgRand            	db          'Could not open /dev/urandom', 0x0a
        errMsgRandLen          	equ         $-errMsgRand

        randSrc            	    db          '/dev/urandom', 0x0
        randNum         	    db          0

        SSE    			        db	'CPU with SSE2 support is required.', 0x0a
        SSELen 			        equ	$-SSE

        F_mul			        dq	        1000.0		; fixed point multiplier for output

        align 16		        ; align for movapd
        abs_mask            	dq          0x7FFFFFFFFFFFFFFF

section .bss

	    T_var			        resq		1
	    V_var			        resq		1
	    D_var			        resq		1

	    V_int			        resq		1
	    V_fix			        resq		1
	    V_mul			        resq		1

	    D_int			        resq		1
	    D_fix			        resq		1
	    D_mul			        resq		1

	    T1_var			        resq		1
	    T1_int			        resq		1
	    T1_frac			        resq		1

	    T2_var			        resq		1
	    T2_int			        resq		1
	    T2_frac			        resq		1

section .text

	    global _start

_start:	; test for SSE2
        mov rax, 1
        cpuid
        test edx, 1<<26
        jnz _10
        mov rsi, SSE
        mov rdx, SSELen
        call write_out
        jmp exit
_10:	; CLS
	    mov rsi, cls_code
        mov rdx, cls_len
        call write_out        
_20:	; PRINT "TRIP INTO THE FUTURE"
	    mov rsi, text1
	    mov rdx, text1Len
	    call write_out
_30:	; LET T=INT(RND*100+25)
	    mov rax, 100
	    mov rcx, 25
	    call rand_func
	    mov qword [T_var], rdx
_40:	; PRINT "YOU WISH TO RETURN ";T
	    mov rax, qword [T_var]
	    mov r9, text2
	    mov r10, text2Len
	    mov r11, text2_s
	    call display_line_int
_50:	; PRINT "YEARS INTO THE FUTURE."
_60:	; PRINT
	    mov rsi, text3
	    mov rdx, text3Len
	    call write_out
_70:	; PRINT "SPEED OF SHIP (0-1)"
	    mov rsi, text4
	    mov rdx, text4Len
	    call write_out
_80:	; INPUT V
	    call read_string
	    mov rsi, inbuf 
	    call string_to_fixed
	    mov qword [V_int], rax
	    mov qword [V_fix], rcx
	    mov qword [V_mul], rdx
_90:	; IF V>=1 OR V<=0 THEN GOTO 70
	    cmp rax, 1
	    jge _70
	    cmp rcx, 0
	    jle _70
_100:	; PRINT "DISTANCE OF TRIP"
	    mov rsi, text5
	    mov rdx, text5Len
	    call write_out
_110:	; INPUT D
	    call read_string
	    mov rsi, inbuf 
	    call string_to_fixed
	    mov qword [D_int], rax
	    mov qword [D_fix], rcx
	    mov qword [D_mul], rdx
_120: 	; LET T1=D/V
	    cvtsi2sd xmm1, [V_fix]	; convert fixed point of V to double
	    cmp qword [V_mul], 0	; avoid divide-by-zero 
	    je ._120_1
	    cvtsi2sd xmm2, [V_mul]
	    divsd xmm1, xmm2		; xmm1 = V=V_fix/V_mul
._120_1:
	    cvtsi2sd xmm0, [D_fix]	; convert fixed point of D to double
	    cmp qword [D_mul], 0
	    je ._120_2
	    cvtsi2sd xmm2, [D_mul]
	    divsd xmm0, xmm2		; xmm0 = D=D_fix/D_mul
._120_2:
	    divsd xmm0, xmm1		; xmm0 = T1 = D/V
	    movsd [T1_var], xmm0	; save T1 for later
	    ; load back into registers here, fixed point
	    cvttsd2si rax, xmm0		; extract integer portion of T1 to rax, 'truncate'
	    cvtsi2sd xmm4, rax		; restore integer portion
	    subsd xmm0, xmm4		; xmm0 now contains fractional portion of T1
	    mulsd xmm0, [F_mul]		; convert it to fixed point
	    cvtsd2si rbx, xmm0		; extract fixed point rep. of fraction of T1 to rbx, 'round'
	    mov qword [T1_int], rax
	    mov qword [T1_frac], rbx
_130:	; LET T2=T1/SQR(1-V*V)
	    mulsd xmm1, xmm1		; xmm1 = V*V
	    mov rax, 1
	    cvtsi2sd xmm2, rax
	    subsd xmm2, xmm1		; xmm2 = 1-V*V
	    sqrtsd xmm1, xmm2		; xmm1 = SQR(1-V*V)
	    movsd xmm3, [T1_var]	; xmm3 = T1
	    divsd xmm3, xmm1		; xmm3 = T2 = T1/SQR(1-V*V)
	    movsd [T2_var], xmm3	; save T2
	    cvttsd2si rax, xmm3		; convert to fixed point as above
	    cvtsi2sd xmm1, rax
	    subsd xmm3, xmm1
	    mulsd xmm3, [F_mul]
	    cvtsd2si rbx, xmm3
	    mov qword [T2_int], rax
	    mov qword [T2_frac], rbx
_140:	; PRINT "YOU TOOK ";T1;"YEARS"
	    mov r14, qword [T1_int]
	    mov r15, qword [T1_frac]
	    mov r9, text6
	    mov r10, text6Len
	    mov r11, text6_s
	    mov r12b, ' '
	    call display_line_dec
	    mov rsi, text6_2
	    mov rdx, text6_2Len
	    call write_out
_150:	; PRINT "AND ARRIVED ";T2;"YEARS"
	    mov r14, qword [T2_int]
	    mov r15, qword [T2_frac]
	    mov r9, text7
	    mov r10, text7Len
	    mov r11, text7_s
	    mov r12b, ' '
	    call display_line_dec
	    mov rsi, text6_2
	    mov rdx, text6_2Len
	    call write_out
_160:	; PRINT "IN THE FUTURE."
	    mov rsi, text8
        mov rdx, text8Len
        call write_out
_170:	; IF T1>50 THEN GOTO 210
	    cmp qword [T1_int], 50
	    jg _210				; being lazy here; should also look at T1_frac
_180:	; IF ABS(T-T2)<=5 THEN PRINT "YOU ARRIVED ON TIME"
    	; do ABS(T-T2)
	    mov rax, qword [T_var]
	    cvtsi2sd xmm0, rax		    ; xmm0 = T
	    movsd xmm1, [T2_var]		; xmm1 = T2
	    subsd xmm0, xmm1		    ; xmm0 = T-T2
	    movapd xmm1, [abs_mask]      
        andpd xmm0, xmm1            ; xmm0 = ABS(T-T2)
	    ; do the comparison. 
	    ; More laziness. Will figure out how cmpsd works on another day
	    cvtsd2si rax, xmm0
	    cmp rax, 5	; ABS(T-T2)<=5
	    jg _190
	    ; print 'YOU ARRIVED ON TIME'
	    mov rsi, text9
        mov rdx, text9Len
        call write_out
	    jmp _200		            ; jmp exit
_190:	; IF ABS(T-T2)>5 THEN PRINT "NOT EVEN CLOSE"
	    mov rsi, text10
        mov rdx, text10Len
        call write_out
_200:	; STOP
	    jmp exit
_210:	; PRINT "YOU DIED ON THE WAY"
	    mov rsi, text11
        mov rdx, text11Len
        call write_out
_220: 	; STOP
exit:
	    mov rax, 0x3c       
	    mov rdi, 0
	    syscall


	    ; random number function. Pulls a byte from /dev/urandom which is used as a random number
	    ; >= 0 and < 1. Pass in a multiplier to this in rax, and an offset to add in rcx.
rand_func:
	    push rbp
	    mov rbp, rsp

	    ; on entry, rax is the multiplier, rcx is the offset.
	    ; in practice it's ax and cx, as it will only work for small numbers
	    push rcx
	    push rax

	    ; open the source of randomness
	    mov rax, 2              ; 'open'
	    mov rdi, randSrc        ; pointer to filename
	    mov rsi, 0              ; flags: 0 is O_RDONLY on my system
	    mov rdx, 0              
	    syscall
        
	    cmp rax, -2             ; file not found           
	    je open_error
	    cmp rax, -13            ; permission denied
	    je open_error

	    mov rbx, rax            ; save the file descriptor

	    ; read a byte
	    mov rax, 0              ; 'read'
	    mov rdi, rbx            ; file descriptor
	    mov rsi, randNum        ; memory location to read to
	    mov rdx, 1              ; read 1 byte
	    push rbx               
	    syscall
	    pop rbx

	    ; close it
	    mov rax, 3              ; 'close'
	    mov rdi, rbx            ; file descriptor
	    syscall

	    ; some fixed-point math. 
	    ; say we have 8 bits of fractional part, and that is the
	    ; random number obtained above, so 0<=rand<1
	    movzx rbx, byte [randNum]   
	    pop rax
	    ; multiply the number in rax by 256 to maintain the fixed-point math.
	    shl rax, 8
	    imul bx
	    ; because the result is in dx:ax, dx already contains the integer portion of the random number.
	    ; so don't have to divide by 256*256.
	    ; add the offset in rcx
	    pop rcx
	    add rdx, rcx

	    pop rbp
	    ret

open_error:
	    ; display a simple message and exit if could not open /dev/urandom
	    mov rsi, errMsgRand
	    mov rdx, errMsgRandLen
	    call write_out
	    jmp exit

	; display a text string followed by an integer number
display_line_int:
	    push rbp
	    mov rbp, rsp

	    ; 1. Convert number to string in scratch buffer
	    mov r8, 10		    	; we divide repeatedly by 10 to convert number to string
	    mov rdi, scratchend		; start from the end of the scratch buffer and work back
	    mov rcx, 0		    	; this will contain the final number of chars
itoa_inner:
	    dec rdi			    	; going backwards in memory
	    mov rdx, 0		    	; set up the division: rax already set coming into procedure
	    div r8			    	; divide by ten
	    add rdx, 0x30	    	; offset the remainder of the division to get the required ascii char
	    mov [rdi], dl			; write the ascii char to the scratch buffer
	    inc rcx			    	; keep track of the number of chars produced
	    cmp rcx, scratchLen		; try not to overfeed the buffer
	    je itoa_done			; break out if we reach the end of the buffer 
	    cmp rax, 0		    	; otherwise keep dividing until nothing left 
	    jne itoa_inner
itoa_done:
	    ; 2. Copy contents of scratch buffer into correct place in output string
	    ; rdi now points to beginning of char string and rcx is the number of chars
	    ; copy number into display buffer
	    mov rsi, rdi
	    mov rdi, r11            ; r11 is set coming into procedure, points to where in memory the number string should go
	    ; rcx already set from above
	    mov r8, rcx;		    ; preserve number of chars in number string 
	    rep movsb		        ; copy the number string to the output buffer
	    mov byte [rdi], 0x0a	; and put a newline on the end of it
show_num:
	    ; 3. Write the complete final string to stdout
	    mov rsi, r9		    	; pointer to final char buffer, r9 is set coming into procedure
	    ; calculate number of chars to display
	    mov rdx, r10 			; length of the preamble, r10 set coming into procedure
	    add rdx, r8		    	; plus length of the number string we just made
	    inc rdx			    	; plus one for newline char
	    mov rax, 1		    	; write
	    mov rdi, 1		    	; to stdout
	    syscall             	; execute

	    pop rbp
	    ret                 	; done


	    ; read in a string from the terminal up until newline
read_string:
	    push rbp
	    mov rbp, rsp

	    ; player is going to enter something in the terminal
	    mov rcx, 0		; count number of chars entered
get_char:
	    ; read a char into the buffer
	    mov rax, 0		; read
	    mov rdi, 0		; from stdin
	    mov rdx, 1		; 1 char
	    mov rsi, inbuf	; calculate the current offset into input buffer
	    add rsi, rcx	; fill it up one char at a time until newline entered
	    push rsi		; preserve the pointer
	    push rcx		; and the counter
	    syscall
	    pop rcx			; restore
	    pop rsi
	    cmp rax, 0		; check for nothing read
	    je exit;		; for now just quit
	    inc rcx			; increment counter
	    movzx rax, byte [rsi]	; check for newline entered
	    cmp rax, 0x0a
	    je done_read		; break out of loop when user hits return 
	    cmp rcx, inbuf_len
	    jge exit;		    ; let's not read beyond the end of the buffer
	    jmp get_char		; continue
done_read:
	    mov byte [rsi], 0

	    pop rbp
	    ret

	; convert a string in decimal point notation to a number in fixed point
string_to_fixed:
	    push rbp
	    mov rbp, rsp

	    xor rax, rax		; integer part of number
	    xor rbx, rbx		; fixed-point divisor (power of 10)
	    xor rcx, rcx		; fixed point representation of number
	    xor rdx, rdx		; increment for decimal point location
.loop_top:
	    movzx r8, byte [rsi]	; get next char in rsi
	    cmp r8, 0		    ; is it null? then end
	    je .done_string
	    cmp r8, '.'		    ; have we reached the decimal point?
	    jne .check_num
	    mov rax, rcx		; save off integer part of number
	    mov rdx, 1		    ; now we have fractional part, shift decimal point by 1 each number
	    jmp .next_char
.check_num:
	    cmp r8, 0x30		; check if char is below '0'
	    jl .next_char		; skip
	    cmp r8, 0x39		; check if char is above '9'
	    jg .next_char		; skip 
	    sub r8, 0x30		; adjust to number representation
	    imul rcx, 10		; make space for next number to be added in
	    add rcx, r8		    ; accumulate number in rcx
	    add rbx, rdx		; inc the number of decimal places if in fractional part
.next_char:
	    inc rsi			    ; move onto the next digit in the string
	    jmp .loop_top
.done_string:
	    cmp rbx, 0		    ; calculate the fixed-point divisor
	    jne .loop_div
	    mov rax, rcx
	    jmp .done_all
.loop_div:	
	    imul rdx, 10
	    dec rbx
	    jnz .loop_div
.done_all:
	    ; rax should be the integer part of the entered number
	    ; rcx the full fixed point number
	    ; rdx the divisor for rcx to convert it back to a real

	    pop rbp
	    ret

	; displays a text string followed by a number with a decimal point
display_line_dec:
	    push rbp
	    mov rbp, rsp

	    ; convert number to string.
	    ; r14 is integer part, r15 is fractional part
.setup:
	    mov r8, 10		    ; divide by ten 
	    mov rdi, scratchend	; start from the end of the buffer and work back
	    xor rcx, rcx		; rcx will be number of chars in the buffer
	    mov rax, r15		; start with fractional part
.inner1:
	    dec rdi			    ; going backwards in the buffer
	    xor rdx, rdx		; this will be the string char of the number
	    div r8			    ; divide the number by 10
	    add rdx, 0x30		; offset to ascii char of the number
	    mov byte [rdi], dl	; write it to the buffer
	    inc rcx			    ; increase the number of chars in the buffer
	    cmp rcx, scratchLen	; don't go beyond end of buffer
	    je .done
	    cmp rax, 0		    ; if nothing left, move on
	    jne .inner1		    ; otherwise continue
	    ; with the fractional part in, put in the decimal point
	    dec rdi
	    mov byte [rdi], '.'
	    inc rcx
	    ; now do the integer part
	    mov rax, r14
.inner2:			        ; same process as above
	    dec rdi
	    xor rdx, rdx
	    div r8
	    add rdx, 0x30
	    mov byte [rdi], dl
	    inc rcx
	    cmp rcx, scratchLen
	    je .done
	    cmp rax, 0
	    jne .inner2
.done:
	    ; copy contents of scratch buffer to output string
        mov rsi, rdi
        mov rdi, r11            ; r11 is set coming into procedure, points to where in memory the number string should go
        ; rcx already set from above
        mov r8, rcx;		    ; preserve number of chars in number string 
        rep movsb		        ; copy the number string to the output buffer
        mov byte [rdi], r12b	; and put whatever's in r12b on the end of it
.show_num:
        ; 3. Write the complete final string to stdout
        mov rsi, r9		    	; pointer to final char buffer, r9 is set coming into procedure
        ; calculate number of chars to display
        mov rdx, r10 			; length of the preamble, r10 set coming into procedure
        add rdx, r8		    	; plus length of the number string we just made
        inc rdx			    	; plus one for end char
        mov rax, 1		    	; write
        mov rdi, 1		    	; to stdout
        syscall             	; execute

	    pop rbp
	    ret

	    ; write a string to stdout
write_out:
        mov rax, 1          
        mov rdi, 1         
	    syscall
	    ret
 

				
				

Asteroid Belt

The main challenge presented by this game was replicating the INKEY$ command. Previous games allowed the player whatever time they wanted to enter numbers, which was easily achieved with the read syscall, but this game requires the player to enter input within a time window. After some experimentation, this was achieved using the poll syscall on STDIN with a timeout, followed by a read if any input was detected. There was a further issue, which is that terminals seem to buffer input until the return key is pressed. To overcome this, the ioctl syscall was used to switch off canonical mode in the terminal, which makes it send input as soon as it appears, rather than buffering it. While in there, input echo is switched off also. To complete the effect, ANSI codes are used to switch off the cursor in the terminal.

In this game FOR loops are used to create a delay. Since looping to fifty (as in lines 290 and 300) would execute in nanoseconds in assembly on modern CPUs, the nanosleep syscall is used to create a reasonable delay.


; Asteroid Belt
; Adapted from "Computer Spacegames" by Daniel Isaaman and Jenny Tyler, Usborne Publishing
; nasm -g -f elf64 asteroid_belt.asm
; ld -o asteroid_belt asteroid_belt.o

section .data

	text1		        db	      'ASTEROID BELT', 0x0a
	text1Len	        equ	      $-text1
	text2		        db	      'CRASHED INTO ASTEROID', 0x0a
	text2Len	        equ	      $-text2
	text3		        db	      'YOU DESTROYED IT', 0x0a
	text3Len	        equ	      $-text3
	text4		        db	      'NOT STRONG ENOUGH', 0x0a
	text4Len	        equ	      $-text4
	text5		        db	      'TOO STRONG', 0x0a
	text5Len	        equ	      $-text5
	text6		        db	      'YOU HIT '
	text6Len	        equ	      $-text6
	text6_s		        times 10 db 0
	text7		        db	      'OUT OF 10', 0x0a
	text7Len	        equ	      $-text7

	lf		            db	      0x0a      ; line feed
	star		        db	      '*'
	tab		            db	      0x09

	; ANSI code to clear the screen
    cls_code            db        0x1b, '[2J', 0x1b, '[H'
    cls_len             equ       $-cls_code

	; ANSI code to switch on and off the cursor
	cursor_off 	        db 	      0x1b, '[?25l'
	cursor_off_len	    equ	      $-cursor_off
	cursor_on 	        db 	      0x1b, '[?25h'
	cursor_on_len	    equ	      $-cursor_on

	; for nanosleep
	; struct timespec
	tv_sec			    dq	      0
	tv_nsec			    dq	      0	

	; for poll
	; struct pollfd ("man 2 poll")
	fd			        dd	      0	; STDIN
	events			    dw	      1	; POLLIN, as per poll.h
	revents			    dw	      0

	; a scratch buffer for number to string conversion
    scratch		    	times 10 db 0
	scratchLen		    equ       $-scratch
	scratchend		    db        0

	; take random numbers from /dev/urandom
	errMsgRand         	db        'Could not open /dev/urandom', 0x0a
	errMsgRandLen      	equ       $-errMsgRand

	randSrc        	    db        '/dev/urandom', 0x0
	randNum        	    db        0

section .bss

	S_var			    resq	  1
	G_var			    resq	  1
	I_var			    resq	  1
	A_var			    resq	  1
	D_var			    resq	  1
	N_var			    resq	  1
	Q_var			    resb	  1

	inbuf			    resb	  1

	termios			    resb 	  36	; for terminal settings

section .text

	global _start

_start:
        ; use ANSI to turn off the cursor
        mov rax, 1
        mov rdi, 1
        mov rsi, cursor_off
        mov rdx, cursor_off_len
        syscall
        ; get the current terminal settings and flip some bits
        ; specifically, for canonical mode and echo
        mov rax, 16			    ; ioctl
        mov rdi, 0			    ; STDIN
        mov rsi, 0x5401			; TCGETS in ioctls.h
        mov rdx, termios		; put terminal settings in this buffer
        syscall
        mov eax, 10			    ; for bitmask, ICANON|ECHO (termbits.h)
        not eax				    ; mask off
        and dword [termios+12], eax	; offset for c_lflag
        ; set the terminal
        mov rax, 16			    ; ioctl
        mov rdi, 0			    ; STDIN
        mov rsi, 0x5402			; TCSETS in ioctls.h
        mov rdx, termios		; write updated terminal settings
        syscall
_10:	; PRINT "ASTEROID BELT"
        mov rsi, text1
        mov rdx, text1Len
        call write_out
        ; insert a pause here to stop the title from being 
        ; instantly wiped by the CLS in line 40
        mov qword [tv_sec], 2	; sleep for 2 seconds
        mov rax, 0x23		    ; nanosleep
        mov rdi, tv_sec
        mov rsi, 0
        syscall
_20:	; LET S=0
        mov qword [S_var], 0
_30:	; FOR G=1 TO 10
        mov qword [G_var], 1
_40:	; CLS
        mov rsi, cls_code
        mov rdx, cls_len
        call write_out        
_50:	; LET A=INT(RND*18+1)
        mov rax, 9	             ; Changed to 9 (from 18) to accommodate terminal limitations
        mov rcx, 1
        call rand_func
        mov qword [A_var], rdx
_60:	; LET D=INT(RND*12+1)
        mov rax, 12
        mov rcx, 1
        call rand_func
        mov qword [D_var], rdx
_70:	; LET N=INT(RND*9+1)
        mov rax, 9
        mov rcx, 1
        call rand_func
        mov qword [N_var], rdx
_80:	; FOR I=1 TO D
        mov qword [I_var], 1
_90:	; PRINT
        mov rsi, lf
        mov rdx, 1
        call write_out
_100:	; NEXT I
        mov al, byte [D_var]
        cmp byte [I_var], al
        je _110
        inc qword [I_var]
        jmp _90
_110:	; FOR I=1 TO N
        mov qword [I_var], 1
_120:	; IF I<>1 AND I<>4 AND I<>7 THEN GOTO 150
        cmp qword [I_var], 1
        je _130
        cmp qword [I_var], 4
        je _130
        cmp qword [I_var], 7
        je _130
        jmp _150
_130:	; PRINT
        mov rsi, lf
        mov rdx, 1
        call write_out
_140:	; PRINT TAB(A)
        mov rcx, qword [A_var]      ; write tab to stdout A times
_140_1:	
        push rcx
        mov rsi, tab
        mov rdx, 1
        call write_out
        pop rcx 
        loop _140_1
_150:	; PRINT "*";
        mov rsi, star
        mov rdx, 1
        call write_out
_160:	; NEXT I
        mov al, byte [N_var]
        cmp byte [I_var], al
        je _170
        inc qword [I_var]
        jmp _120
_170:	; PRINT
        mov rsi, lf
        mov rdx, 1
        call write_out
_180:	; FOR I=1 TO 10
_190:	; LET Q=VAL("0"+INKEY$)
        ; use poll syscall to check stdin with timeout
        mov rax, 7		    ; poll
        mov rdi, fd		    ; pointer to pollfd struct
        mov rsi, 1		    ; number of fds to monitor (1 in this case)
        mov rdx, 1500		; timeout in milliseconds 
        syscall
_200:	; IF Q<>0 THEN GOTO 240
        cmp rax, 0
        jg _240
_210:	; NEXT I
_220:	; PRINT "CRASHED INTO ASTEROID"
        mov rsi, text2
        mov rdx, text2Len
        call write_out
_230:	; GOTO 290
        jmp _290
_240:	; IF Q<>N THEN GOTO 270
        ; read in the entered char
        mov rax, 0
        mov rdi, 0
        mov rdx, 1
        mov rsi, Q_var
        syscall
        mov rax, qword [N_var]
        add rax, 0x30
        cmp byte [Q_var], al
        jne _270
_250:	; PRINT "YOU DESTROYED IT"
        mov rsi, text3
        mov rdx, text3Len
        call write_out
_260:	; LET S=S+1
        inc qword [S_var]
        jmp _290
_270:	; IF Q<N THEN PRINT "NOT STRONG ENOUGH"
        jg _280	
        mov rsi, text4
        mov rdx, text4Len
        call write_out
        jmp _290
_280:	; IF Q>N THEN PRINT "TOO STRONG"
        mov rsi, text5
        mov rdx, text5Len
        call write_out
_290:	; FOR I=1 TO 50
_300:	; NEXT I
        ; Lines 290 and 300 are intended to create a delay, but will execute in negligable time in assembly
        ; use nanosleep with some small time instead
        mov qword [tv_sec], 0	
        mov qword [tv_nsec], 800000000
        mov rax, 0x23		; nanosleep
        mov rdi, tv_sec
        mov rsi, 0
        syscall
        ; clear out the terminal input buffer here in case player accidentally pressed a key during the delay
        mov rax, 7		; poll
        mov rdi, fd		
        mov rsi, 1		
        mov rdx, 0		; just check and return immediately 
        syscall
        cmp rax, 0		; if nothing entered, move on
        je _310
        mov rax, 0		; otherwise read the key from stdin
        mov rdi, 0		; thereby clearing the buffer
        mov rdx, 1
        mov rsi, inbuf
        syscall
_310:	; NEXT G
        cmp byte [G_var], 10
        je _320
        inc qword [G_var]
        jmp _40
_320:	; PRINT "YOU HIT ";S;" OUT OF 10"
        mov rax, qword [S_var]              
        mov r9, text6
        mov r10, text6Len
        mov r11, text6_s
        mov r12, 0x20
        call display_line
        mov rsi, text7
        mov rdx, text7Len
        call write_out
_330:	; STOP
exit:
        ; restore the terminal
        or dword [termios+12], 10
        mov rax, 16
        mov rdi, 0
        mov rsi, 0x5402
        mov rdx, termios
        syscall
        ; turn the cursor back on
        mov rax, 1
        mov rdi, 1
        mov rsi, cursor_on
        mov rdx, cursor_on_len
        syscall

        ; exit
        mov rax, 0x3c       
        mov rdi, 0
        syscall

        
        ; random number function. Pulls a byte from /dev/urandom which is used as a random number
        ; >= 0 and < 1. Pass in a multiplier to this in rax, and an offset to add in rcx.
rand_func:
        push rbp
        mov rbp, rsp

        ; on entry, rax is the multiplier, rcx is the offset.
        ; in practice it's ax and cx, as it will only work for small numbers
        push rcx
        push rax

        ; open the source of randomness
        mov rax, 2              ; 'open'
        mov rdi, randSrc        ; pointer to filename
        mov rsi, 0              ; flags: 0 is O_RDONLY on my system
        mov rdx, 0              
        syscall
        
        cmp rax, -2             ; file not found           
        je open_error
        cmp rax, -13            ; permission denied
        je open_error

        mov rbx, rax            ; save the file descriptor

        ; read a byte
        mov rax, 0              ; 'read'
        mov rdi, rbx            ; file descriptor
        mov rsi, randNum        ; memory location to read to
        mov rdx, 1              ; read 1 byte
        push rbx               
        syscall
        pop rbx

        ; close it
        mov rax, 3              ; 'close'
        mov rdi, rbx            ; file descriptor
        syscall

        ; some fixed-point math. 
        ; say we have 8 bits of fractional part, and that is the
        ; random number obtained above, so 0<=rand<1
        movzx rbx, byte [randNum]   
        pop rax
        ; multiply the number in rax by 256 to maintain the fixed-point math.
        shl rax, 8
        imul bx
        ; because the result is in dx:ax, dx already contains the integer portion of the random number.
        ; so don't have to divide by 256*256.
        ; add the offset in rcx
        pop rcx
        add rdx, rcx

        pop rbp
        ret

open_error:
        ; display a simple message and exit if could not open /dev/urandom
        mov rsi, errMsgRand
        mov rdx, errMsgRandLen
        call write_out
        jmp exit


        ; display an output string with a number on the end
display_line:
        push rbp
        mov rbp, rsp

        ; 0. Figure out if the number in rax is negative and respond appropriately
        xor r13, r13
        mov r13b, '+'
        cmp rax, 0
        jge itoa_setup
        ; is negative, setup sign
        mov r13b, '-'
        ; do abs
        cqo
        xor rax, rdx
        sub rax, rdx
itoa_setup:
        ; 1. Convert number to string in scratch buffer
        mov r8, 10		    	    ; we divide repeatedly by 10 to convert number to string
        mov rdi, scratchend		    ; start from the end of the scratch buffer and work back
        mov rcx, 0		    	    ; this will contain the final number of chars
itoa_inner:
        dec rdi			    	    ; going backwards in memory
        mov rdx, 0		    	    ; set up the division: rax already set coming into procedure
        div r8			    	    ; divide by ten
        add rdx, 0x30	    		; offset the remainder of the division to get the required ascii char
        mov [rdi], dl			    ; write the ascii char to the scratch buffer
        inc rcx			    	    ; keep track of the number of chars produced
        cmp rcx, scratchLen		    ; try not to overfeed the buffer
        je itoa_done			    ; break out if we reach the end of the buffer 
        cmp rax, 0		    	    ; otherwise keep dividing until nothing left 
        jne itoa_inner
itoa_done:
        ; 2. Copy contents of scratch buffer into correct place in output string
        ; rdi now points to beginning of char string and rcx is the number of chars
        ; copy number into display buffer
        mov rsi, rdi
        mov rdi, r11            	; r11 is set coming into procedure, points to where in memory the number string should go
        mov r8, rcx
        cmp r13b, '+'
        je past_minus
        mov byte [rdi], r13b		; sign byte
        inc rdi
        inc r8
past_minus:
        ; rcx already set from above
        rep movsb		            ; copy the number string to the output buffer
        mov byte [rdi], r12b		; and put whatever's in r12b at the end of it
show_num:
        ; 3. Write the complete final string to stdout
        mov rsi, r9		    	    ; pointer to final char buffer, r9 is set coming into procedure
        ; calculate number of chars to display
        mov rdx, r10 			    ; length of the preamble, r10 set coming into procedure
        add rdx, r8		    	    ; plus length of the number string we just made
        inc rdx			    	    ; plus one for end char
        mov rax, 1		    	    ; write
        mov rdi, 1		    	    ; to stdout
        syscall             		; execute

        pop rbp
        ret                 		; done


	; write a string to stdout
write_out:
        mov rax, 1          
        mov rdi, 1         
        syscall
        ret

				
				

Alien Snipers

With the INKEY$ function figured out, the Alien Snipers game is straightforward.


; Alien Snipers
; Adapted from "Computer Spacegames" by Daniel Isaaman and Jenny Tyler, Usborne Publishing
; nasm -g -f elf64 alien_snipers.asm
; ld -o alien_snipers alien_snipers.o

section .data

        text1		    db		'ALIEN SNIPERS', 0x0a, 0x0a
        text1Len	    equ		$-text1
        text2		    db		'DIFFICULTY (1-10)', 0x0a
        text2Len	    equ		$-text2
        text3		    db		0x0a, 'YOU HIT '
        text3Len	    equ		$-text3
        text3_s		    times 10 db 0
        text4		    db		'/10', 0x0a
        text4Len	    equ		$-text4
        text5		    db		' ', 0x09
        text5Len	    equ		1	
        text5_s		    times 10 db 0

        cls_code        db      	0x1b, '[2J', 0x1b, '[H'
        cls_len         equ     	$-cls_code

        ; ANSI code to switch on and off the cursor
        cursor_off 	db 		0x1b, '[?25l'
        cursor_off_len	equ		$-cursor_off
        cursor_on 	db 		0x1b, '[?25h'
        cursor_on_len	equ		$-cursor_on

        lf		db		0x0a

        ; input buffer for player input
        inbuf		    times 10 db 0
        inbuf_len    	equ $-inbuf
        null_char    	db 		0

        ; for poll
        ; struct pollfd ("man 2 poll")
        fd			    dd	0	; STDIN
        events			dw	1	; POLLIN, as per poll.h
        revents			dw	0

        ; a scratch buffer for number to string conversion
        scratch		    times 10 db 0
        scratchLen		equ $-scratch
        scratchend		db 0

        ; take random numbers from /dev/urandom
        errMsgRand         	db      'Could not open /dev/urandom', 0x0a
        errMsgRandLen      	equ     $-errMsgRand

        randSrc 	       	db      '/dev/urandom', 0x0
        randNum	        	db      0

section .bss

        S_var		     resq		1
        G_var		     resq		1
        D_var		     resq		1
        N_var		     resq		1
        L_str_var	     resq		1
        I_var		     resb		1

        termios		     resb 		36	; for terminal settings

section .text

        global _start

_start:
_10:	; CLS
        mov rsi, cls_code
        mov rdx, cls_len
        call write_out        
_20:	; PRINT "ALIEN SNIPERS"
_30:	; PRINT
        mov rsi, text1
        mov rdx, text1Len
        call write_out
_40:	; PRINT "DIFFICULTY (1-10)"
        mov rsi, text2
        mov rdx, text2Len
        call write_out
_50:	; INPUT D
        call read_string
        mov rsi, inbuf 
        call string_to_num
        mov [D_var], cl
_60:	; IF D<1 OR D>10 THEN GOTO 50
        cmp qword [D_var], 1
        jl _40		; seems to make more sense to go back to line 40?
        cmp qword [D_var], 10
        jg _40
_70:	; LET S=0
        mov qword [S_var], 0
        ; at this point, change the terminal for INKEY$
        ; use ANSI to turn off the cursor
        mov rax, 1
        mov rdi, 1
        mov rsi, cursor_off
        mov rdx, cursor_off_len
        syscall
        ; get the current terminal settings and flip some bits
        ; specifically, for canonical mode and echo
        mov rax, 16			    ; ioctl
        mov rdi, 0			    ; STDIN
        mov rsi, 0x5401			; TCGETS in ioctls.h
        mov rdx, termios		; put terminal settings in this buffer
        syscall
        mov eax, 10			    ; for bitmask, ICANON|ECHO (termbits.h)
        not eax				    ; mask off
        and dword [termios+12], eax	; offset for c_lflag
        ; set the terminal
        mov rax, 16			     ; ioctl
        mov rdi, 0			    ; STDIN
        mov rsi, 0x5402			; TCSETS in ioctls.h
        mov rdx, termios		; write updated terminal settings
        syscall
_80:	; FOR G=1 TO 10
        mov qword [G_var], 1
_90: 	; LET L$=CHR$(INT(RND*(26-D)+38))
        mov rax, 26
        sub rax, qword [D_var]
        mov rcx, 0x41	; not 38: "man ascii" gives the char codes
        call rand_func
        mov qword [L_str_var], rdx
_100:	; LET N=INT(RND*D+1)
        mov rax, qword [D_var]	
        mov rcx, 1
        call rand_func
        mov qword [N_var], rdx
_110:	; CLS
        mov rsi, cls_code
        mov rdx, cls_len
        call write_out        
_120:	; PRINT
        mov rsi, lf
        mov rdx, 1
        call write_out
_130:	; PRINT L$,N
        mov rax, qword [L_str_var]
        mov byte [text5], al
        mov rax, qword [N_var]
        mov r9, text5
        mov r10, 2
        mov r11, text5_s
        mov r12, ' ' 		; this is necessary to overwrite any lingering 0 in the buffer should the player
                            ; choose a difficulty of 10. Really the entire buffer should be cleared each time.
        call display_line	
_140:	; FOR I=1 TO 20+D*5
_150: 	; LET I$=INKEY$
        mov rax, 7		; poll
        mov rdi, fd		; pointer to pollfd struct
        mov rsi, 1		; number of fds to monitor (1 in this case)
        imul rdx, qword [D_var], 1000 ; attempt to come up with a timeout based on difficulty
        add rdx, 2000		; timeout milliseconds 
        syscall
_160:	; IF I$<>"" THEN GOTO 190
_170:	; NEXT I
        cmp rax, 0
        jg _190
_180:	; GOTO 200
        jmp _200
_190:	; IF I$=CHR$(CODE(L$)+N) THEN LET S=S+1
        mov rax, 0		; Read in the entered char
        mov rdi, 0
        mov rdx, 1
        mov rsi, I_var
        syscall
        mov rax, qword [L_str_var]
        add rax, qword [N_var]
        and byte [I_var], 11011111b	; force uppercase for comparison
        cmp al, byte [I_var]
        jne _200
        inc qword [S_var]
_200:	; NEXT G
        cmp byte [G_var], 10
        je _210
        inc qword [G_var]
        jmp _90
_210:	; PRINT "YOU HIT ";S;"/10"
        mov rax, qword [S_var]              
        mov r9, text3
        mov r10, text3Len
        mov r11, text3_s
        xor r12, r12
        call display_line
        mov rsi, text4
        mov rdx, text4Len
        call write_out
_220:	; STOP
exit:
        ; restore the terminal
        or dword [termios+12], 10
        mov rax, 16
        mov rdi, 0
        mov rsi, 0x5402
        mov rdx, termios
        syscall
        ; turn the cursor back on
        mov rax, 1
        mov rdi, 1
        mov rsi, cursor_on
        mov rdx, cursor_on_len
        syscall
        ; exit
        mov rax, 0x3c       
        mov rdi, 0
        syscall


; display an output string with a number on the end
display_line:
        push rbp
        mov rbp, rsp

        ; 0. Figure out if the number in rax is negative and respond appropriately
        xor r13, r13
        mov r13b, '+'
        cmp rax, 0
        jge itoa_setup
        ; is negative, setup sign
        mov r13b, '-'
        ; do abs
        cqo
        xor rax, rdx
        sub rax, rdx
itoa_setup:
        ; 1. Convert number to string in scratch buffer
        mov r8, 10		    	    ; we divide repeatedly by 10 to convert number to string
        mov rdi, scratchend		    ; start from the end of the scratch buffer and work back
        mov rcx, 0		    	    ; this will contain the final number of chars
itoa_inner:
        dec rdi			    	    ; going backwards in memory
        mov rdx, 0		    	    ; set up the division: rax already set coming into procedure
        div r8			    	    ; divide by ten
        add rdx, 0x30	    		; offset the remainder of the division to get the required ascii char
        mov [rdi], dl			    ; write the ascii char to the scratch buffer
        inc rcx			    	    ; keep track of the number of chars produced
        cmp rcx, scratchLen		    ; try not to overfeed the buffer
        je itoa_done			    ; break out if we reach the end of the buffer 
        cmp rax, 0		    	    ; otherwise keep dividing until nothing left 
        jne itoa_inner
itoa_done:
        ; 2. Copy contents of scratch buffer into correct place in output string
        ; rdi now points to beginning of char string and rcx is the number of chars
        ; copy number into display buffer
        mov rsi, rdi
        mov rdi, r11            	; r11 is set coming into procedure, points to where in memory the number string should go
        mov r8, rcx
        cmp r13b, '+'
        je past_minus
        mov byte [rdi], r13b		; sign byte
        inc rdi
        inc r8
past_minus:
        ; rcx already set from above
        rep movsb		            ; copy the number string to the output buffer
        cmp r12b, 0
        je show_num
        mov byte [rdi], r12b		; and put whatever's in r12b at the end of it
show_num:
        ; 3. Write the complete final string to stdout
        mov rsi, r9		    	    ; pointer to final char buffer, r9 is set coming into procedure
        ; calculate number of chars to display
        mov rdx, r10 			    ; length of the preamble, r10 set coming into procedure
        add rdx, r8		    	    ; plus length of the number string we just made
        inc rdx			    	    ; plus one for end char
        mov rax, 1		    	    ; write
        mov rdi, 1		    	    ; to stdout
        syscall             		; execute

        pop rbp
        ret                 		; done

        
read_string:
        push rbp
        mov rbp, rsp

        ; player is going to enter something in the terminal
        mov rcx, 0		    ; count number of chars entered
get_char:
        ; read a char into the buffer
        mov rax, 0		    ; read
        mov rdi, 0		    ; from stdin
        mov rdx, 1		    ; 1 char
        mov rsi, inbuf		; calculate the current offset into input buffer
        add rsi, rcx		; fill it up one char at a time until newline entered
        push rsi		    ; preserve the pointer
        push rcx		    ; and the counter
        syscall
        pop rcx			    ; restore
        pop rsi
        cmp rax, 0		    ; check for nothing read
        je done_read		; for now just quit
        inc rcx			    ; increment counter
        movzx rax, byte [rsi]	; check for newline entered
        cmp rax, 0x0a
        je done_read		; break out of loop when user hits return 
        cmp rcx, inbuf_len
        jge done_read		; let's not read beyond the end of the buffer
        jmp get_char		; continue
done_read:
        mov byte [rsi], 0

        pop rbp
        ret

string_to_num:
        push rbp
        mov rbp, rsp

        mov rcx, 0			            ; rcx will be the final number
atoi_loop:
        movzx rbx, byte [rsi]       	; get the char pointed to by rsi
        cmp rbx, 0x30               	; Check if char is below '0' 
        jl exit
        cmp rbx, 0x39               	; Check if char is above '9'
        jg exit
        sub rbx, 0x30               	; adjust to actual number by subtracting ASCII offset to 0
        add rcx, rbx                	; accumulate number in rcx
        movzx rbx, byte [rsi+1]     	; check the next char to see if the string continues
        cmp rbx, 0                  	; string should be null-terminated
        je done_string		            ; if it's null we're done converting
        imul rcx, 10                	; multiply rcx by ten
        inc rsi                     	; increment pointer to get next char when we loop
        jmp atoi_loop
done_string:
        ; rcx is the number
        pop rbp
        ret

        
        ; random number function. Pulls a byte from /dev/urandom which is used as a random number
        ; >= 0 and < 1. Pass in a multiplier to this in rax, and an offset to add in rcx.
rand_func:
        push rbp
        mov rbp, rsp

        ; on entry, rax is the multiplier, rcx is the offset.
        ; in practice it's ax and cx, as it will only work for small numbers
        push rcx
        push rax

        ; open the source of randomness
        mov rax, 2              ; 'open'
        mov rdi, randSrc        ; pointer to filename
        mov rsi, 0              ; flags: 0 is O_RDONLY on my system
        mov rdx, 0              
        syscall
        
        cmp rax, -2             ; file not found           
        je open_error
        cmp rax, -13            ; permission denied
        je open_error

        mov rbx, rax            ; save the file descriptor

        ; read a byte
        mov rax, 0              ; 'read'
        mov rdi, rbx            ; file descriptor
        mov rsi, randNum        ; memory location to read to
        mov rdx, 1              ; read 1 byte
        push rbx               
        syscall
        pop rbx

        ; close it
        mov rax, 3              ; 'close'
        mov rdi, rbx            ; file descriptor
        syscall

        ; some fixed-point math. 
        ; say we have 8 bits of fractional part, and that is the
        ; random number obtained above, so 0<=rand<1
        movzx rbx, byte [randNum]   
        pop rax
        ; multiply the number in rax by 256 to maintain the fixed-point math.
        shl rax, 8
        imul bx
        ; because the result is in dx:ax, dx already contains the integer portion of the random number.
        ; so don't have to divide by 256*256.
        ; add the offset in rcx
        pop rcx
        add rdx, rcx

        pop rbp
        ret

open_error:
        ; display a simple message and exit if could not open /dev/urandom
        mov rsi, errMsgRand
        mov rdx, errMsgRandLen
        call write_out
        jmp exit


; write a string to stdout
write_out:
        mov rax, 1          
        mov rdi, 1         
        syscall
        ret