Tny is a tiny virtual console. It can execute sequences of bytes that have a special meaning. These sequences are called "ROMs" and the bytes "machine code". To make writing ROMs easier for Humans, Tny also has an assembly language. The language exposes special words that translate directly to machine instructions. These are called mnemonics. In this book, we will learn about the mnemonics to build simple games with Tny.
All Tny ROMs must start with `0 0` or other numbers.
0 0 LIT 1 INC
There is a lot going on in the above. Let's work through it.
First, LIT is an instruction that tells Tny to push the next number into the parameter stack (pstack). The parameter stack is a special part of memory in which we can push or pop values. It's used to pass parameters, thus its name.
Then, INC is another instruction that increases by one the top of the pstack.
If you haven't done so, execute the above program. You should see that the pstack now has 02 stored in it.
Manipulating the stack
Now that we've seen the stack, let's see how we can manipulate it.
LIT lets you push the next number to the stack.
You can read about the other instructions in the glossary below. Let's look at DUP. It says that the purpose is the "Duplicate the top of the stack". There's another column called "Effect" which says:
n -- n n
This is a way to indicate the effect of DUP on the stack. DUP requires a number to be on the stack. This number is the "n" represented on the left of "--". After its execution, the stack now has "n" twice.
Learning how to read stack effects is a great way to quickly see what parameters an instruction needs on the stack, and how it will modify the stack.
All numbers are expressed in hexadecimal.
LIT f \ push the decimal value 15 to the stack
Let's add two numbers together:
LIT 1 LIT 1 ADD
Now, let's multiply the result by two:
LIT 2 MUL
How about we divide it by 4?
LIT 4 DIV
If you ever need a random number, you can use the RND instruction which will place on the stack a random number in the 0 255 range.
Labels and addresses
When your program gets assembled, every mnemonic gets mapped to a corresponding machine code value, which fits in a byte. Since every instruction fits in a single byte, we can refer to a particular place in your ROM by its offset. In Tny, this offset is called an address.
Imagine the following program:
0 0 LIT 5 LIT 5 ADD
The address of the first LIT instruction is 2. The address of the ADD instruction is 6. Let's now look at the following program:
0 0 start: LIT 5 LIT 5 ADD
In the above code, we added a label called start. It could have been called anything else. When Tny's preprocessor finds a label, it remembers its name and location, so that we can use this location later on.
Let's look at the following program:
0 0 LIT @start JMP start: LIT 5 LIT 5 ADD BRK LIT 2
Let's try to understand what happens. When executed, the Instruction Pointer (IP) starts at address 2. It finds LIT, so it pushes the number that follows it into the stack. In this case, @start represents the memory address of the start label, which is 5. Our stack now has a 5 in it. Then it finds a JMP instruction. The JMP instruction tells the IP to take the value of the top of the stack, so 5, and to carry on. Our program then executes the LIT instructions, then the ADD one, and finally BRK. BRK is a special instruction that stops the execution of the program. Meaning that the remaining LIT 2 instruction won't be executed.
There are different ways to jump. For example, JMR will store the position of the next instruction in a special stack called the return stack. This allows to resume the execution with the RET instruction.
Drawing on the screen
The screen tny is 16x16, meaning that a screen position can be encoded in one byte. The address of the top left pixel is 00, and the address of the bottom right if ff. The high nibble represents the rows, and the low nibble the columns. For example, the pixel at the 4th row and 10th column is 4a.
A pixel can be either on or off.
0 0 LIT 11 LIT 1 SET
Here, we set the pixel at offset 11.
Like most consoles, 45M has a controller. A very basic one! It has your usual up left down right keys, as well as A and B which are mapped the the X and C key of the keyboard, respectively.
You can push to the stack the value of the current key being pressed with the KEY instruction. The value has the following format:
0 0 0 B A R L D U
B: the B buttonA: the A button R: the right arrow L: the left arrow D: the down arrow U: the up arrow
Remember, each ROM starts with two values: 0 0. These values are actually memory addresses. Let's take the following example:
@frame 0 BRK frame: LIT 0 LIT 1 SET
Let's start with memory address 0. It's the screen vector. The value at this address will be called 60 times per second. In the example above, we have set it to the frame label.
Now let's continue with memory address 1. The controller vector:
@frame @key BRK frame: LIT 0 LIT 1 SET key: KEY
The key vector will be executed anytime a key from the controller has been pressed.
It's possible to use use labels, jumps, and the return stack in order to create a loop. Here is a full example:
0 0 LIT 0 LIT 10 PSH PSH do: RSI LIT 1 SET PUL INC PSH RSI RSJ LTH LIT @do JCN PUL PUL POP POP
We start by setting the screen and controller vectors to 0. Then we push 0 and 10 to the stack. They are the index and the limit respectively. The next two instructions, PSH and PSH, move the index and limit to the return stack. We use the return stack as a way to keep track of the current iteration. After that, we create a label called "do". That's the start of the loop.
The first line of the loop turns on a pixel on the first line, based on the current index. It uses the RSI instruction, which copies the top of the return stack to the parameter stack.
The next line increments the index by one by. To do that it pulls it from the return stack, increments it, and pushes it back.
The next line is the test. We compare the top two values of the return stack by using RSI and RSJ with LTH. We then jump back to our "do" label when the comparaison is true.
Finally, we pop the index and limit from the return stack.
Now that we've seen an example, here is a template:
LIT index LIT limit PSH PSH do: # instructions go here PUL INC PSH RSI RSJ LTH LIT @do JCN PUL PUL POP POP
Use FRM to control the screen vector's FPS
The screen vector is called 60 times per second, which might be too much for some programs. To change that, we can leverage the FRM and MOD instructions. Let's take an example:
2 0 CLS FRM LIT 1e MOD LIT 0 NEQ LIT @end JCN LIT 0 LIT 1 SET end: BRK
This program starts by setting the screen vector to the address 2. 60 times per second, Tny's Instruction Pointer will be set to the address 2 and will start to execute each instruction sequentially. The first one, CLS, clears the screen. The following line will jump to the "end" label if the current frame should be skipped. To know how many frames should be skipped, we need to compute the following:
frame_skip = 60 / desired_fps
In our example, we want 2 frames per second, so the number of frames to skip is:
LIT 3c LIT 2 DIV
Which is equal to 30 (1e). That's why the 3rd line pushes the current frame as well as 1e to the stack and applies a modulo. We then push 0 and NEQ to make sure to jump our when the current frame isn't a multiple of 1e.
Here's a snippet you can use to controle the FPS:
FRM LIT frame_skip MOD LIT 0 NEQ LIT @end JCN # Perform computation end: BRK
Move a pixel around with the keyboard
Let's see how we can move a pixel around. For that, we will use the KEY instruction together with JCN. Here's an example:
@screen @ctrl screen: CLS LIT pos: 0 LIT 1 SET BRK ctrl: KEY LIT 1 EQU LIT @up JCN KEY LIT 2 EQU LIT @down JCN KEY LIT 4 EQU LIT @left JCN KEY LIT 8 EQU LIT @right JCN BRK up: LIT @pos LDA LIT 10 SUB LIT @pos STA BRK down: LIT @pos LDA LIT 10 ADD LIT @pos STA BRK left: LIT @pos LDA DEC LIT @pos STA BRK right: LIT @pos LDA INC LIT @pos STA BRK BRK
As always, we start by setting the screen and controller vector. Any time an arrow key is pressed, the controller vector will get called. In there, we use KEY, which pushes the value of the controller to the stack. We then test its value and jump to the correct label based on it. Notice that we modify directly the value at the pos address.