Skip to content

Tetris code comment

elzibus edited this page Aug 8, 2020 · 41 revisions

Introduction

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!

Comments on the code

| 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.

⁉️ Challenge: why is it not 200 bytes then?

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.

⁉️ Challenge: how is each piece encoded? It is very nice and compact.

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.

⁉️ Challenge: look at the rotate> values and try to reverse engineer how Pablo did it.

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:

⁉️ In the context of the game, there is an issue with one of the pieces. Can you find it? Then make a pull request :)


#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.

⁉️ What is the value on the top of the stack when the rest of the word is executed?

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'.

Clone this wiki locally