-
Notifications
You must be signed in to change notification settings - Fork 4
Tetris code comment
This is an exploration of an english translation of the tetris.r3 code Pablo wrote.
Full path in the repository: https://github.com/phreda4/r3d4/blob/master/r3/dev/tetris.en.r3
The translation was done on a version captured on 25JUL2020.
Comments are written after each block of code.
To the reader: we expect you have looked at the documentation a bit. Let us know where you are lost in the text below so that we can improve it. Thanks!
| tetris r3
| PHREDA 2020
|-------------------
Lines that begin with the vertical bar character (or pipe character) prefix are comments.
They are ignored by the interpreter/compiler UNLESS they are formatted in a special way. There are no such special comments in this source file, you can find some in main.r3 at the root of the distribution, look for lines that start with |WIN|, |LIN|..
^r3/lib/gui.r3
Here, another prefix is introduced: the ^ character.
It lets you import words that are defined in other files.
For example, in gui.r3 the word whin is defined with two colons :: instead of a single one.
This means that when gui.r3 is imported in another r3 file, the whin word is made available.
This enables the creation of modules that comprise words for specific aspects: gui, math, dated, etc..
The gui module contains graphical user interface related functionality.
^r3/lib/rand.r3
The rand module contain random numbers related functionality.
#grid * 800 | 10 * 20 * 4
New prefix: the # character defines variables.
For tetris we need to keep track of what has been drawn where.
This variable contains space for a 2d array of 10 * 20 which is the dimension of the grid in which the game is played.
It is an array of 800 bytes.
Spoiler warning: solution
Each entry takes up 4 bytes to encode a color.#colors 0 $ffff $ff $ff8040 $ffff00 $ff00 $ff0000 $800080
colors is another variable that contains a list of colors encoded in RGB hex format.
0 is black
$ff is blue
etc..
#pieces
1 5 9 13
1 5 9 8
1 5 9 10
0 1 5 6
1 2 4 5
1 4 5 6
0 1 4 5
tetris is played with pieces of specific shape. They all comprise 4 square blocks (tetraminoes).
The list above contains 7 blocks of 4 numbers. The first piece is: 1 5 9 13.
Spoiler warning: hint 1
It is not about the bits in each value.Spoiler warning: hint 2
Look at this table:
0 | 1 | 2 | 3 |
4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 |
Spoiler warning: solution
Okay, here is the solution. Let's take this piece (the next to last) : 1 4 5 6.
Take each value and locate it in the grid from the previous hint.
■ | |||
■ | ■ | ■ | |
This is how each piece is encoded!
#rotate>
8 4 0 0
9 5 1 13
10 6 2 0
0 7 0 0
Now for the rotation!
In the game, when you press the UP key, the piece that is in play rotates.
So each piece has 4 orientations but above we encoded only one of them.
We therefore need a way to calculate piece rotations.
Spoiler warning: hint 1
It looks like rotate> defines a 4 by 4 grid..Spoiler warning: hint 2
Let's take the numbered grid we showed earlier:
0 | 1 | 2 | 3 |
4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 |
Now let's put the numbers in rotate> in a similar grid:
8 | 4 | 0 | 0 |
9 | 5 | 1 | 13 |
10 | 6 | 2 | 0 |
0 | 7 | 0 | 0 |
Spoiler warning: hint 3
The 4 by 4 grid encodes a mapping for each block to its rotated position..Spoiler warning: Solution
The center of rotation is position 5 in the grid. Rotation is counter-clockwise.To know how a block from a piece is rotated, take its value and look up in rotate> what its rotated position is:
0 ➡️ 8 | 1 ➡️ 4 | 2 ➡️ 0 | 3 ➡️ 0 |
4 ➡️ 9 | 5 ➡️ 5 | 6 ➡️ 1 | 7 ➡️ 13 |
8 ➡️ 10 | 9 ➡️ 6 | 10 ➡️ 2 | 11 ➡️ 0 |
12 ➡️ 0 | 13 ➡️ 7 | 14 ➡️ 0 | 15 ➡️ 0 |
Example, the "L" piece which is encoded as 1 5 9 10 becomes 4 5 6 2. When drawn:
■ | |||
■ | ■ | ■ | |
#mask
$000 $001 $002 $003
$100 $101 $102 $103
$200 $201 $202 $203
$300 $301 $302 $303
#player 0 0 0 0
The current piece is encoded in the 4 values associated to the variable player, each piece comprises 4 values as explained earlier.
#playeryx 0
#playercolor 0
playercolor contains the color of the current piece, ie one of the values in the colors list defined above
#points 0
#nextpiece 0
defines what piece will be played after the current one
#speed 300
defines the speed at which the current piece is falling
:packed2xy | n -- x y
dup $ff and 4 << 50 +
swap 8 >> 4 << 100 +
;
The ':' prefix is used to define a new word. An action that once defined can be used just like DUP, SWAP, ..
Player coordinates are 'packed' which means that (x,y) are made to fit in a single number.
This code consumes one packed value from the stack and produces two values on the stack one for x and one for y.
Stack at the beginning of the word:
n |
.. |
.. means that there possibly are more values below n.
n is at the top of the stack. Let's execute word by word:
After DUP:
n |
n |
.. |
After $ff:
$ff |
n |
n |
.. |
After AND:
n AND $FF |
n |
.. |
After 4:
4 |
n AND $FF |
n |
.. |
After <<:
(n AND $FF) << 4 |
n |
.. |
After 50:
50 |
(n AND $FF) << 4 |
n |
.. |
After +:
((n AND $FF) << 4) + 50 |
n |
.. |
After SWAP:
n |
((n AND $FF) << 4) + 50 |
.. |
This is a long-winded explanation, after some practice there is no need to proceed this way each time.
Spoiler warning: solution
((n>>8)<<4)+100 |
((n AND $FF) << 4) + 50 |
.. |
Graphically, a packed value looks like this: $YX where Y and X are 8 bit values.
The top of the stack contains Y * 16.
The value below the top of stack is X * 16.
The multiplication by 16 is used because each block in a piece is 16 * 16 pixels.
Note also that the origin for graphics is top, left. Increasing x to the right, increasing y to the bottom.
:draw_block | ( x y -- )
2dup op
over 15 + over pline
over 15 + over 15 + pline
over over 15 + pline
pline poli ;
This code draws a square on screen, its top left corner is at (x,y).
:visit_block | ( y x -- y x )
a@+ 0? ( drop ; ) 'ink !
2dup or packed2xy draw_block ;
visit_block is a new word. It uses a conditional.
:draw_grid | ( --- )
'grid >a
0 ( $1400 <?
1 ( 11 <?
visit_block
1 + ) drop
$100 + ) drop ;
:rotate_block | ( adr -- v adr )
dup @ 2 << 'rotate> + @ swap ;
:rotate_piece | ( --- )
'player
rotate_block !+
rotate_block !+
rotate_block !+
rotate_block ! ;
:inmask | ( v1 -- v2)
2 << 'mask + @ ;
:translate_block | ( v -- )
inmask playeryx + ;
:draw_player_block | ( v -- )
translate_block packed2xy draw_block ;
:draw_player | ( --- )
playercolor 'ink !
'player
@+ draw_player_block
@+ draw_player_block
@+ draw_player_block
@ draw_player_block ;
This word draws the current piece.
Starts by setting the drawing color - which is saved in the playercolor variable.
Then then the player address is put on the top of the stack.
The four values the compose the player piece are walked and each corresponding block drawn.
@+ takes an address fetches the value it contains and puts on the stack address of the next element and the value that was fetched:
adr becomes (adr+4) (value in adr)
:nthcolor | ( n -- color )
2 << 'colors + @ ;
This is a standard pattern. The nthcolor word gets a number and gets the nth value in a table.
2 << get 4n 'colors + adds the start of the array to the 4n @ fetches the value in the array.
In other words, this gets the nth value in a 32 bits array.
:draw_nextpiece_block | ( v -- )
inmask 15 + packed2xy draw_block ;
:draw_nextpiece
nextpiece dup nthcolor 'ink !
1 - 4 << 'pieces +
@+ draw_nextpiece_block
@+ draw_nextpiece_block
@+ draw_nextpiece_block
@ draw_nextpiece_block ;
:rand1.7 | -- rand1..7
( rand dup 16 >> xor $7 and 0? drop ) ;
:new_piece
nextpiece
'player
over 1 - 4 << 'pieces +
4 move | dst src cnt
nthcolor 'playercolor !
5 'playeryx !
rand1.7 'nextpiece !
;
:packed2gridptr | coord -- realcoord
dup $f and 1 - | x
swap 8 >> 10 * +
2 << 'grid + ;
Computes the address in the grid from packed coordinates.
:block_collision? | pos -- 0/pos
$1400 >? ( drop 0 ; )
dup packed2gridptr @ 1? ( 2drop 0 ; ) drop
$ff and
0? ( drop 0 ; )
10 >? ( drop 0 ; )
;
:piece_collision? | ( v -- v/0 )
'player
@+ translate_block pick2 + block_collision? 0? ( nip nip ; ) drop
@+ translate_block pick2 + block_collision? 0? ( nip nip ; ) drop
@+ translate_block pick2 + block_collision? 0? ( nip nip ; ) drop
@ translate_block over + block_collision? 0? ( nip ; ) drop
;
A piece collides with existing blocks if any block in the current piece collides.
:piece_rcollision? | ( -- 0/x )
'player
@+ 2 << 'rotate> + @ translate_block block_collision? 0? ( nip ; ) drop
@+ 2 << 'rotate> + @ translate_block block_collision? 0? ( nip ; ) drop
@+ 2 << 'rotate> + @ translate_block block_collision? 0? ( nip ; ) drop
@ 2 << 'rotate> + @ translate_block block_collision? ;
This word looks a lot like the one before.. it would be nice to find a factorization to capture the similarity. Can't think of any right now..
#combo
#combop 0 40 100 300 1200
Points won by combination.
:removeline |
'grid dup 40 + swap a> pick2 - 2 >> move>
-1 'speed +!
4 'combo +! ;
:testline
'combop 'combo !
'grid >a
0 ( $1400 <?
0 1 ( 11 <?
a@+ 1? ( rot 1 + rot rot ) drop
1 + ) drop
10 =? ( removeline ) drop
$100 + ) drop
combo @ 'points +!
;
:write_block | ( v -- )
translate_block packed2gridptr playercolor swap ! ;
write_block writes a single block in the grid. It computes the grid location with the packed2gridptr word and writes playercolor in that location.
:stopped
'player
@+ write_block
@+ write_block
@+ write_block
@ write_block
testline
new_piece
;
The piece is stopped by static blocks (from previous plays). We need to write 4 blocks by walking the piece contents. Once this is done we need to check if full horizontal lines have appeared. Then a new piece comes into play.
:logic
$100 piece_collision? 0? ( drop stopped ; )
'playeryx +!
;
:translate
piece_collision? 'playeryx +! ;
:rotate
piece_rcollision? 0? ( drop ; ) drop
rotate_piece ;
The rotate word performs the rotation. If there is a collision then nothing happens, otherwise the rotate_piece word is called.
#ntime
#dtime
:game | ( --- )
cls home
$ff00 'ink !
20 20 atxy "Tetris R3" print
$444444 'ink !
128 70 286 96 fillrect
166 326 62 96 fillrect
$ffffff 'ink !
360 100 atxy points "%d" print
draw_grid
draw_player
draw_nextpiece
msec dup ntime - 'dtime +! 'ntime !
dtime speed >? ( dup speed - 'dtime !
logic
) drop
key
>esc< =? ( exit )
<dn> =? ( 250 'dtime +! )
<ri> =? ( 1 translate )
<le> =? ( -1 translate )
<up> =? ( rotate )
drop ;
:start | ( --- )
0 'points !
300 'speed !
msec 'ntime ! 0 'dtime !
rerand
rand1.7 'nextpiece !
'game onshow
;
the start words initializes variables, initializes the random number generator and, with onshow, calls the game word. This is the 'game loop' for the game.
: start ;
The single colon followed by a space has a special meaning: it is the 'boot' location. This is where the execution of the program starts.
When you run this program, this word is located and 'start' is the first word that is executed as it is the word directly after 'boot'.