compudanzas/src/uxn_tutorial_day_3.gmo

738 lines
24 KiB
Plaintext

# uxn tutorial: day 3, conditional jumps and the keyboard/controller
this is the third section of the {uxn tutorial}!
here we introduce the use of the controller device in the varvara uxn computer: this will allow us to add interactivity to our programs, and to start discussing control flow in uxntal.
we also talk about logic and stack manipulation instructions in uxntal.
# the controller device
the controller device in the varvara computer allows us to read inputs from the keyboard and/or from controller buttons.
the definition of its ports would look as follows in a typical program:
```
|80 @Controller [ &vector $2 &button $1 &key $1 ]
```
## the button byte
the button byte encodes in each of its eight bits the state of eight different "buttons", based on the NES controller layout.
=> https://wiki.nesdev.com/w/index.php/Standard_controller standard NES controller
numbering the bits from right to left, and from 0 to 7, the corresponding keys (and NES buttons) are:
& * 0: Ctrl (button A)
& * 1: Alt (button B)
& * 2: Shift (Select button)
& * 3: Esc (Start button)
& * 4: Up
& * 5: Down
& * 6: Left
& * 7: Right
+ <table>
+ <tr><th>bit 7</th><th>bit 6</th><th>bit 5</th><th>bit 4</th><th>bit 3</th><th>bit 2</th><th>bit 1</th><th>bit 0</th></tr>
+ <tr><td>Right</td><td>Left</td><td>Down</td><td>Up</td><td>Esc (Start)</td><td>Shift (Select)</td><td>Alt (B)</td><td>Ctrl (A)</td></tr>
+ </table>
enconding the states in this allows us to press and read many of these keys at the same time.
## the key byte
the key byte stores the ascii code of the keyboard key that is being pressed at the moment.
## the controller vector
a vector refers to an address in main memory where uxn is assigned to jump to when a specific event happens.
in the case of the controller vector, this specific event consists in whenever a key is pressed or released.
the following line of code would assign the absolute address of the label on-controller to the controller vector:
```
;on-controller .Controller/vector DEO2
```
uxn will "jump" to that address whenever a key is pressed or released.
let's see next how would that work!
# control flow: vector subroutines
so far our uxntal programs have followed a linear flow: they start at address 0100, and they end at the first BRK instruction that is found.
we can think of these programs as setup routines: they setup the system colors, they might draw or print some things, and then they leave uxn "waiting". what would uxn be waiting for?
yes, an option would be: waiting for keyboard input!
we will start organizing our uxntal programs in terms of subroutines that correspond to different vectors.
each of these subroutines will finish with the BRK instruction, so that they can return uxn to the waiting state.
## controller vector subroutine
for example, the following program takes the sprite drawing we tried in the previous section, but has it ocurring only when a key is pressed:
```
( hello-keyboard.tal )
( devices )
|00 @System [ &vector $2 &pad $6 &r $2 &g $2 &b $2 ]
|20 @Screen [ &vector $2 &width $2 &height $2 &pad $2 &x $2 &y $2 &addr $2 &pixel $1 &sprite $1 ]
|80 @Controller [ &vector $2 &button $1 &key $1 ]
( main program )
|0100
( set system colors )
#2ce9 .System/r DEO2
#01c0 .System/g DEO2
#2ce5 .System/b DEO2
( assign controller vector )
;on-controller .Controller/vector DEO2
BRK
( run this code whenever a key is pressed or released )
@on-controller ( -> )
( set x,y coordinates )
#0008 .Screen/x DEO2
#0008 .Screen/y DEO2
( set sprite address )
;square .Screen/addr DEO2
( draw sprite in the background )
( using color 1 for the outline )
#01 .Screen/sprite DEO
BRK
( sprite )
@square ff81 8181 8181 81ff
```
how do we take different actions depending on the key that was pressed?
first of all, we have to let our program know which key was pressed. in order to achieve that, let's take a look at some uxntal instructions that will help us!
# comparison and logic instructions
## comparison instructions
uxntal has four instructions for comparing the top two elements from the stack:
* EQU: push 01 down into the stack if the top two elements of the stack are equal, 00 otherwise ( a b -- a==b )
* NEQ: push 01 down into the stack if the top two elements of the stack are not equal, 00 otherwise ( a b -- a!=b )
* GTH: push 01 down into the stack if the first element is greater than the second, 00 otherwise ( a b -- a>b )
* LTH: push 01 down into the stack if the first element is less than the second, 00 otherwise ( a b -- a>b )
for example, the following code will read the value of the controller key, and push into the stack a flag corresponding to it being equal to character 'a':
```
.Controller/key DEI ( read key )
LIT 'a ( push ascii code of character 'a' )
EQU ( compare both bytes and push 01 if they are the same, 00 if not )
```
EQU2, NEQ2, GTH2 and LTH2 will work in the same way, but comparing shorts instead of bytes.
## logic instructions
uxntal has three bitwise logic instructions.
they can work as logic operators that have as operands the results given by the comparison instructions we discussed above:
* AND: perform a bitwise AND with the top two elements of the stack, and push down the result ( a b -- a&b )
* ORA: perform a bitwise OR with the top two elements of the stack, and push down the result ( a b -- a|b )
* EOR: perform a bitwise exclusive-OR with the top two elements of the stack, and push down the result ( a b -- a^b )
AND2, ORA2, EOR2 will work in the same way, but with shorts instead of bytes.
### AND
the following will push down into the stack a flag that indicates if the key byte is between 30 and 39 inclusive, using 01 to answer "true", and 00 to answer "false":
```
.Controller/key DEI ( read key )
#2f GTH ( is it greater than 2f? push flag into the stack )
.Controller/key DEI ( ready key )
#3a LTH ( is it less than 3a? push flag into the stack )
AND ( apply an AND to the flags in the stack, and push the result in the stack )
```
that the instruction is bitwise means that it applies the AND operation to each of the bits of the operands.
if both flags were "true":
```
0000 0001 ( true )
AND 0000 0001 ( true )
----------
0000 0001 ( true )
```
if any (or both) of the flags were "false":
```
0000 0001 ( true )
AND 0000 0000 ( false )
----------
0000 0000 ( false )
```
### OR
the following code will push a flag down into the stack if the key byte is either '1' or 'a':
```
.Controller/key DEI ( read key )
LIT '1 EQU ( is it '1'? push flag into the stack )
.Controller/key DEI ( read key )
LIT 'a EQU ( is it 'a'? push flag into the stack )
ORA ( apply an OR to the flags in the stack, and push the result in the stack )
```
when any of the flags are true, the flag will be true:
```
0000 0001 ( true )
OR 0000 0000 ( false )
----------
0000 0001 ( true )
```
### EOR
an exclusive OR can be used to invert the value of a flag, by using a "mask" where the bit(s) that we'd like to invert are set as 1.
for example, the folowing code will push a flag corresponding to the key being greater than or equal to 20, by calculating first if it's less than 20, and then inverting the result:
```
.Controller/key DEI ( read key )
#20 LTH ( is it less than 20? push flag into the stack )
#01 EOR ( invert leftmost bit of the flag and push the result into the stack )
```
when the original flag is true, which means that the value is less than 20, the EOR will invert it and make it false: the value is NOT greater than or equal to 20:
```
0000 0001 ( true )
EOR 0000 0001 ( mask )
----------
0000 0000 ( false )
```
when the original flag is false, which means that the value is NOT less than 20, the EOR will invert it and make it true: the value is greater than or equal to 20:
```
0000 0000 ( false )
EOR 0000 0001 ( mask )
----------
0000 0001 ( true )
```
# control flow: conditional jumps
ok, so how can we use these flags in order to have conditional expressions in our programs?
let's introduce another set of new instructions to have uxn break its linear flow
## instructions for jumps
* JMP: unconditionally jump to the address in the stack ( addr -- )
* JCN: take an address and a value from the stack, and jump to the address if the value is not 00; otherwise continue with the next instruction ( value addr -- )
in the byte mode, the addresses that these instructions take are one byte long.
these byte-long addresses are relative and signed: they indicate how many bytes have to be skipped in main memory from the current position of the program counter, either forward (positive) or backwards (negative). the range for these relative addresses is from -128 to 127 inclusive.
in short mode, the addresses that these instructions take are absolute (i.e. two-bytes long), but the value that JCN takes in order to decide is still a byte.
## runes for addresses
there are several runes that refer to addresses and labels. uxnasm reads them and converts them to the corresponding binary values.
in the previous days we talked already about some of them; this is a recap of those, and an introduction of the new ones:
* literal address in zero page: .label (one byte)
* literal address in main memory: ;label (one short)
* literal relative address in main memory: ,label (one byte)
* raw address in main memory: :label (one short)
in order to define labels, we use:
* label definition: @label
* sublabel definition: &sublabel (this sublabel will be a "child" of the previously defined label)
and finally, to refer to labels within our uxntal code, we have the following cases:
* for a main label: use the label name
* for a sublabel: use label/sublabel
* for a local sublabel: use &sublabel
## conditional jump
let's join all of this together!
the following on-controller subroutine, illustrates the use of jumps, drawing our sprite only when the key that was pressed was '1':
```
@on-controller
.Controller/key DEI ( read key )
LIT '1 EQU ( is it '1'? )
,&draw-sprite JCN ( jump to draw-sprite if that's the case )
,&end JMP ( otherwise, jump to the end )
&draw-sprite
( set x,y coordinates )
#0008 .Screen/x DEO2
#0008 .Screen/y DEO2
( set sprite address )
;square .Screen/addr DEO2
( draw sprite in the background )
( using color 1 for the outline )
#01 .Screen/sprite DEO
&end
BRK
```
note the use of sublabels "inside" (after) on-controller.
also note how the expression ,&sublabel corresponds to the relative address that is needed in order to jump to that location in the code, either using JCN or JMP.
## conditional jumps
the following code illustrates the use of many conditions: the color of the sprite changes accordingly if the keys 1, 2 or 3 are pressed.
```
@on-controller
( set x,y coordinates )
#0008 .Screen/x DEO2
#0008 .Screen/y DEO2
( set sprite address )
;square .Screen/addr DEO2
.Controller/key DEI LIT '1 EQU ( is the key '1'? )
,&color-1 JCN ( jump to color-1 if that's the case )
.Controller/key DEI LIT '2 EQU ( is the key '2'? )
,&color-2 JCN ( jump to color-2 if that's the case )
.Controller/key DEI LIT '3 EQU ( is the key '3'? )
,&color-3 JCN ( jump to color-3 if that's the case )
( in any other case, finish )
BRK
&color-1
( draw sprite in the background )
( using color 1 for the outline )
#01 .Screen/sprite DEO
BRK
&color-2
( draw sprite in the background )
( using color 2 for the outline )
#02 .Screen/sprite DEO
BRK
&color-3
( draw sprite in the background )
( using color 3 for the outline )
#03 .Screen/sprite DEO
BRK
BRK
```
note how the conditions are concatenated: whenever a flag is false, JCN allows uxn to continue with the next instruction in memory.
also note that this code is not optimized for size or speed, but for readability.
it would be up to you to, for example, perform arithmetic with the value of the key that was pressed in order to calculate the color to assign to the sprite :)
# stack manipulation
so far we have been using the stack as a place to store operands of instructions and their results, but we haven't used yet the full potential of this data structure in a forth-like environment like uxn.
## stack instructions
uxntal has five instructions that act upon the elements in the stack:
* POP: Remove top element from the stack ( a -- )
* DUP: Duplicate; push a copy of the top element ( a -- a a )
* SWP: Swap; change the order of the top two elements of the stack ( a b -- b a )
* OVR: Over; push a copy of the second top element ( a b -- a b a )
* ROT: Rotate; reorder the top three elements of the stack so that the third one is now at the top ( a b c -- b c a )
in short mode, POP2, DUP2, SWP2, OVR2 and ROT2 perform the same actions but using shorts instead of bytes.
## examples
we'll be using these instructions in many different ways in the following days.
the following are some examples based on snippets of code that we discussed already.
keep in mind that using these instructions may contribute to a code that is hard to follow or read, so it will be always a good idea to use them within macros or to have comments in the code explaining what's happening.
### ascii digit: duplicate and swap
we talked above about this piece of code, that pushes a flag that answers if the key that is pressed has an ascii code between 30 and 39, inclusive (i.e., if a byte has an ascii code corresponding to a decimal digit)
```
.Controller/key DEI ( read key )
#2f GTH ( is it greater than 2f? push flag into the stack )
.Controller/key DEI ( ready key )
#3a LTH ( is it less than 3a? push flag into the stack )
AND ( apply an AND to the flags in the stack, and push the result in the stack )
```
instead of reading the key twice, we could do it once, and then use the DUP instruction to copy the value:
```
.Controller/key DEI DUP ( read and duplicate key )
```
the stack after this would have:
```
key key <- top
```
we can continue adding the first comparison:
```
#2f GTH ( is it greater than 2f? push flag into the stack )
```
after this, the stack would look like:
```
key flag1 <- top
```
in order to perform the second comparison, we need to have the key at the top, not the flag.
how do we achieve that? that's right, using a SWP:
```
SWP ( put key at the top )
```
now the stack looks like:
```
flag1 key <- top
```
and we can proceed with the comparison and the AND
```
#3a LTH ( is it less than 3a? push flag into the stack )
AND ( apply an AND to the flags in the stack, and push the result in the stack )
```
ending with a stack that has the result only:
```
result <- top
```
the complete code would read as:
```
.Controller/key DEI DUP ( read and duplicate key )
#2f GTH ( is it greater than 2f? push flag into the stack )
SWP ( put key at the top )
#3a LTH ( is it less than 3a? push flag into the stack )
AND ( apply an AND to the flags in the stack, and push the result in the stack )
```
the first code is assembled as 13 bytes, and this one is assembled as 12 bytes. maybe not too much of a difference on that front.
however, an advantage is that this new routine now needs its input pushed down into the stack only at its beginning.
in this case, the input is the key that is pressed, but we could easily have as an input any other value from the stack.
this implies that we could write the routine as a macro:
```
%?ASCII-DIGIT { DUP #2f GTH SWP #3a LTH AND } ( byte -- flag )
```
and use it with whatever byte we like:
```
#30 ?ASCII-DIGIT ( pushes 01 down into the stack )
#20 ?ASCII-DIGIT ( pushes 00 down into the stack )
.Controller/key ?ASCII-DIGIT ( pushes a corresponding flag down into the stack )
```
### duplicates for conditionals
another instance where we repeated many reads of the keyboard key was when using the multiple conditionals above.
we could rewrite it using several DUPs and POPs:
```
@on-controller
( set x,y coordinates )
#0008 .Screen/x DEO2
#0008 .Screen/y DEO2
( set sprite address )
;square .Screen/addr DEO2
.Controller/key DEI ( read key )
DUP LIT '1 EQU ( is the key '1'? )
,&color-1 JCN ( jump to color-1 if that's the case )
DUP LIT '2 EQU ( is the key '2'? )
,&color-2 JCN ( jump to color-2 if that's the case )
DUP LIT '3 EQU ( is the key '3'? )
,&color-3 JCN ( jump to color-3 if that's the case )
( in any other case, finish )
POP
BRK
&color-1
( draw sprite in the background )
( using color 1 for the outline )
#01 .Screen/sprite DEO
POP
BRK
&color-2
( draw sprite in the background )
( using color 2 for the outline )
#02 .Screen/sprite DEO
POP
BRK
&color-3
( draw sprite in the background )
( using color 3 for the outline )
#03 .Screen/sprite DEO
POP
BRK
BRK
```
can you tell why we need all those POPs?
# controller button
the last thing we'll discuss today is the use of the controller button byte in the varvara computer.
as we mentioned already, the main difference here is that this byte holds the state of 8 buttons in each of its bits.
depending on our application, we might need to be able to allow for some of these buttons to be pressed at the same time.
in that case, how would we isolate each of the bits to check their state individually?
meet the bitwise AND masks!
## AND mask
an AND mask will have bit(s) set as 1 in the position(s) where we want to keep the value of the original bit(s). in all the other positions, the original bits will be converted to 0.
for example, let's say we want to see if bit number 4, corresponding to the Up button, is on or off, regardless of the state of the other buttons.
our AND mask will have a 1 in bit number 4 (from right to left, and starting at 0), and 0 elsewhere:
```
0001 000: 10
```
what would happen if button A (Ctrl key), with its state in bit 0, is pressed, and nothing else?
```
0000 0001 ( button )
AND 0001 0000 ( mask )
----------
0000 0000 ( result )
```
what happens if the Up button is pressed?
```
0001 0000 ( button )
AND 0001 0000 ( mask )
----------
0001 0000 ( result )
```
and if both Up and Ctrl are pressed?
```
0001 0001 ( button )
AND 0001 0000 ( mask )
----------
0001 0000 ( result )
```
see how the mask allows us to effectively isolate the bit that matters to us, regardless of the sate of the other bits.
applying this mask would be as simple as writing:
```
#10 AND ( apply 0001 000 mask )
```
## example: draw with arrows and Ctrl
=> ./img/screenshot_uxn-draw-with-keyboard.png screenshot of a possible result of running the following program; it shows a trail drawn with filled or outlined squares.
the following uxntal program allows you to draw using the arrows keys and the Ctrl key.
the arrows move the position of a sprite, and pressing Ctrl while moving it will draw it with the inverse colors in fill and stroke.
note the use of AND masks, conditional jumps, and some stack operations!
```
( draw-with-keyboard.tal )
( devices )
|00 @System [ &vector $2 &pad $6 &r $2 &g $2 &b $2 ]
|20 @Screen [ &vector $2 &width $2 &height $2 &pad $2 &x $2 &y $2 &addr $2 &pixel $1 &sprite ]
|80 @Controller [ &vector $2 &button $1 &key $1 ]
( main program )
|0100
( set system colors )
#2ce9 .System/r DEO2
#01c0 .System/g DEO2
#2ce5 .System/b DEO2
( assign controller vector )
;on-controller .Controller/vector DEO2
( set initial x,y coordinates )
#0008 .Screen/x DEO2
#0008 .Screen/y DEO2
( set sprite address )
;square .Screen/addr DEO2
BRK
@on-controller ( -> )
.Controller/button DEI DUP ( read and duplicate button byte )
#01 AND ( isolate bit 0, corresponding to Ctrl )
,&fill JCN ( if the bit is not 0, jump to fill, otherwise continue )
&outline
#01 .Screen/sprite DEO ( draw outline )
,&check-arrows JMP ( continue to check-arrows )
&fill
#04 .Screen/sprite DEO ( draw filled )
&check-arrows
( use button byte from the stack )
DUP #10 AND ( isolate bit 4, corresponding to Up )
,&up JCN ( jump if not 0 )
DUP #20 AND ( isolate bit 5, corresponding to Down )
,&down JCN ( jump if not 0 )
DUP #40 AND ( isolate bit 6, corresponding to Left )
,&left JCN ( jump if not 0 )
DUP #80 AND ( isolate bit 7, corresponding to Right )
,&right JCN ( jump if not 0 )
POP BRK
&up
.Screen/y DEI2 #0008 SUB2 .Screen/y DEO2 ( decrement y )
POP
BRK
&down
.Screen/y DEI2 #0008 ADD2 .Screen/y DEO2 ( increment y )
POP
BRK
&left
.Screen/x DEI2 #0008 SUB2 .Screen/x DEO2 ( decrement x )
POP
BRK
&right
.Screen/x DEI2 #0008 ADD2 .Screen/x DEO2 ( increment x )
POP
BRK
BRK
( sprite )
@square ff81 8181 8181 81ff
```
some possibilities for you to practice:
* modify the code so that it will also respond to you pressing more than one arrow at the same time.
* convert the increments and decrements of the coordinates to macros that take the address of the port as input, and perform an equivalent operation. both of these lines should work using the same macro:
```
.Screen/x INCREMENT
.Screen/y INCREMENT
```
remember that .Screen/x is a literal address in the zero page, i.e. it pushes a byte corresponding to the address of the Screen/x sublabel :)
# practice possibilities
here are some other ideas for you to practice with what we covered today!
* draw a virtual controller that shows which of its buttons, mapped to keyboard keys, are being pressed
* create some kind of typewriter that draws different symbols and moves the drawing cursor depending on the key that was pressed.
* draw a character that changes its state according to the key you pressed. maybe use multiple tiles to draw it?
* create a simple tic-tac-toe board for two players: one key draws a X, another draws a O, and the arrows allow you to choose the cell to draw.
note that for smooth interactive movement it might be better to use the screen vector that is called 60 times per second!
we'll cover it in depth in day 4 of the tutorial!
# instructions of day 3
these are all the uxntal instructions that we discussed today!
## comparison instructions
* EQU: push 01 down into the stack if the top two elements of the stack are equal, 00 otherwise ( a b -- a==b )
* NEQ: push 01 down into the stack if the top two elements of the stack are not equal, 00 otherwise ( a b -- a!=b )
* GTH: push 01 down into the stack if the first element is greater than the second, 00 otherwise ( a b -- a>b )
* LTH: push 01 down into the stack if the first element is less than the second, 00 otherwise ( a b -- a>b )
## bitwise logic
* AND: perform a bitwise AND with the top two elements of the stack, and push down the result ( a b -- a&b )
* ORA: perform a bitwise OR with the top two elements of the stack, and push down the result ( a b -- a|b )
* EOR: perform a bitwise exclusive-OR with the top two elements of the stack, and push down the result ( a b -- a^b )
## jumps
* JMP: unconditionally jump to the address in the stack ( addr -- )
* JCN: take an address and a value from the stack, and jump to the address if the value is not 00; otherwise continue with the next instruction ( value addr -- )
## stack
* POP: Remove top element from the stack ( a -- )
* DUP: Duplicate; push a copy of the top element ( a -- a a )
* SWP: Swap; change the order of the top two elements of the stack ( a b -- b a )
* OVR: Over; push a copy of the second top element ( a b -- a b a )
* ROT: Rotate; reorder the top three elements of the stack so that the third one is now at the top ( a b c -- b c a )
# day 4
in {uxn tutorial day 4} we cover the use of the screen vector in order to create animation, either interactive or not!
we also explore possibilities for using "variables" in uxntal than can help us creating more elaborate programs!
before jumping in, i invite you to keep exploring and to also take a break!
stay tuned!
# support
if you found this tutorial to be helpful, consider sharing it and giving it your {support} :)