8/7/2019 Parallel Tetris
1/19
Parallel Tetris
Introduction
The aim of this project was to create a 2 player version of the popular game Tetris. The
idea was to have each player playing the game on his own screen using the the PS/2
Keyboard for input and at the same time his play would influence the play of hisopponent. By that I mean that when a player cleared lines on his game his opponent
would go up a predefined number of lines (where each line was a random sequence of
blocks) in his game. To add to this each player would be able to to view the state of his
opponents game on his own screen enabling him to make informed decisons in his play.For this I used two Altera De2 Boards which were communicating over the serial port.
The motivation behind this project was that I wanted to do a project which had a visual
component to it and utilised a variety of the resources offered by the DE2 Board. I also
wanted to do a project where some element of networking was involved. The design of agame with communication over the serial port accomplished all of these goals and helped
me learn how to use a variety of components on the Altera Board.
8/7/2019 Parallel Tetris
2/19
High Level Design
I started out this project by researching what projects had already been done on the FPGAon the topic. This was a good starting point as it gave me an idea of what was feasible aI
did not use the code from any of these projects but it gave me a couple of design ideas formy own project which I would like to thank them for. Some of the things which I took
away from these projects is that the groups had successfully managed to implement thealgorithms and game in hardware and software giving me the option of chosing which
direction I wanted to go in with this project. Another design idea I got from these projects
was the idea of using something known as sprites which is the idea of having each of thetetris pieces predefined and then working of that definition when doing any
transformations during the game. There are a lot of different components that were
needed for this project to work. The following sections give an overview of thosecomponents before I get into decribing the actual design and code for the game and how
the different components are connected together.
VGA
As this project is the design of a game, display is one of the most important aspects. Thisrequired a good understanding of the VGA controller so that I could modify it based on
the needs of the game. For the working of the VGA I used the bookRapid PrototypingOf Digital Systems (SOPC Edition) by James Hamblen , Tyson Hall and Michael
Furman.I had a good understanding of the VGA controller from the previous labs butthere was one thing that I had to figure out how to do. This was to change the way the
data was read in from the SRAM so as to get a resolution of 640*480. For this I used
Professor Land's DE2 Harware examples as reference and a previous year Paint Brush
Apllication Project done by Ranjani Chandrsekar and Manu Jain. I would like to thankthem for the help. Both of these pages are linked below. The basic working for this is as
follows. The VGA controller has two 10 bit outputs which are Coord_X and Coord_Y.The SRAM takes in a 18 bit address which can be thought if as 9 bit x coordinate address
and a 9 bit y coordinate address. The SRAM then returns a a 16 bits value stored at that
address. This works fine when doing a 320x240 resolution, but the SRAM would run outof space doing the same color mapping on a 640x480 resolution. To get around this you
have to reduce the color depth to 8 bits/pixel, where 3 bits are for red , 3 bits are for green
and 2 bits are for blue. This way you essentially double your storage. To do this what you
have to do is use the top 9 bits of Coord_X and bottom 9 bits of Coord_Y as addressesinto the SRAM. Then you use the LSB of Coord_X to choose between which half of the
16 bit data from SRAM to use as input into the VGA controller
.
8/7/2019 Parallel Tetris
3/19
PS/2 KEYBOARD
The next important component that I needed to research and incorporate into the design
was the PS/2 keyboard. This is a very important component as well as this was what Iwas using for the joystick esentially.I used the bookRapid Prototyping Of Digital
Systems (SOPC Edition) by James Hamblen , Tyson Hall and Michael Furman againin order to learn about the way the keyboard works. From reading the book I saw how thekeyboard keys were all uniquely identified by their scan codes which are listed as a table
in the book. I used a verilog module from online which has the vhdl equivalent given in tI
adapted their module with my project and had it so that I could read the keyboard scan
codes when I pressed a key and was able to verify that it was working by comparing thecodes to those given in the book mentioned above. I will talk about how I used this in the
overall design to detect key presses in the game when I discuss the design and code in the
later sections.
RS-232 Serial PortAs I was doing a parallel tetris I needed a way to communicate between the two boards. I
used the serial port on the DE2 Board as it was it was fast enough to handle the amount
of data transfer that was invovled and I had experience working with serialcommunication.As I was just using the basic features I just used the ground , receive and
transmit port on the DE2 serial port. I will discuss the code and the setup later but the one
thing that needed to be done in order for the connection to work was that the receive andtransmit pins needed to be switched when connecting the two boards. Ill mention this
again when I describe the setup to run the actual project.
8/7/2019 Parallel Tetris
4/19
Block Diagram
8/7/2019 Parallel Tetris
5/19
The different components are the ones I discussed briefly in the above sections, a NIOS
processor and the different pieces used to make the all the components work together
successfully. I will describe the code in detail in the Design section but the thing to seehere is that this figure represents what runs on one of the boards. The other side runs the
same setup and they are communicating using the serial port as shown in the figure.
Hardware/Software Tradeoffs
I had a couple of design decisons to make in terms of what I wanted to do in harware orsoftware. Doing the image rendering and game algorithm completely in hardware would
offer speed but would be harder to implement and debug as compared to the NIOS. The
NIOS provided easy compile time and debugging features not available in hardware. Soas speed was the limiting factor I did a couple of tests where I wrote to SRAM and saw
how quick the changes were seen. As the speed was acceptable for a game to be played I
decided to do my implementation of the game in software using the NIOS. Now, whendisplaying the opponents grid on the same monitor ,as I was doing no computation on the
data, even though it would have been easier to do the display using the NIOS it wouldhave slowed the game down. So for displaying the opponents grid I did the logic all in
hardware.
Program/Hardware Design
There are a lot of components in the project. So I will break down the project into the
essential parts needed and explain the code for them.As most of the game design is donein software the first thing to show is the SOPC builder design and components which
were also seen in the Block Doagram section above. This will be a good reference to
explain the code for the different components.
8/7/2019 Parallel Tetris
6/19
NIOS
First I just wanted to give a brief description of the NIOS I used. As this project was notgoing to require significant portion of the boards logic elements I decided to go with the
NIOS II/f in the SOPC builder which has a lot more features and is faster. This is a 32-bit
8/7/2019 Parallel Tetris
7/19
RISC processor with a 4kb instruction cache and a 2 kb data cache. It also has branch
prediction and some other features. As I was using SDRAM for the program which is 8
Mb, memory usage would not an issue. I inclued a jtag_uart compnent as it enables printstatements which help in debugging. The following sections explain the design
Writing to SRAMAs mentioned before the SRAM now has data stored as 8 bits/pixel. To write the SRAM I
used the function writesram which is given below.This was taken from the Paint BrushApplication which I mentioned earlier and linked.
This function basically drives the buses for the x coordinate , y coordinate and data to be
written to the SRAM. The hardware then uses the top nine bits of the outputxcoord from
the NIOS concatenated with the bottom 9 bits of the outputycoord as the address at whichto write in the SRAM. The data to be written as you would expect is the
outputsramdatabus from the NIOS. The thing to be careful about is that when you write
to SRAM you have to delay for some cycles as the SRAM only gets the value if the VGAcontroller is not accessing the SRAM. This is same as making sure that the Hsync and
Vsync signals from the VGA are not high. There are a number of ways to do this but i
used the way which was used in the Paint Brush Application project which is to have adelay() function that counts to 150. Another small design trick that I used from the Paint
Project was that when you loop to write pixel values to a small area of the screen you
increment the x coordinate by 2 as you are writing 2 pixel values with every write. To
write 2 pixels in one value, you write one value, shift the variable left by 8 bits and thenOR with the second value. I used this for my initializescreen() function which I adapted
from the Paintbrush Application project.
Game DesignNow that I had figured out how to write to the screen using the NIOS to get a 640x480
resolution I had to design the basic tetris pieces. I started out the design by realising that
each tetris piece can be broken down into 4 smaller squares. So first I decided how big I
wanted to make the small square and how big I wanted the playing grid to be. I decidedto make the small square 20 pixels x 20 pixels and the playing grid 20 x 10 which
essentially means a grid of 200 small squares. The following figure illustrates this point.
8/7/2019 Parallel Tetris
8/19
Now that I had decided this, I wrote a createunit() function which takes an x coordinatevalue, y coordinate value and a color as inputs and draws a 20x20 pixel square with the xand y coordinates as the center of the square.
Next design decison was to decide how to represent a tetris piece. I decided on having a
struct coordinate which which has an x coordinate and y coordinate field. Then I made a
tetris_piece which had 4 coordinate structs defining the locations of the 4 small squaresthat make up a tetris piece. It then had a type field which identifies what kind of tetris
piece it is as there are seven distinct type of tetris pieces. There was a state field to keep
track of the orientation of the piece which helps with figuring out how to rotate the piece.The final field was a color field as each piece was a different color. Now that I had a
struct defining a piece I wrote a function draw which took a struct tetris_piece as inputand drew the piece as defined by the coordinates on the screen.
Tetris Pieces Design
Now that I had the basic struct for a piece and a function to draw the pieces I decided to
define a 7x11 array called piecessprites in which each row defines one of the 7 distinct
tetris piece. The 11 values are the 4 pairs of x-y coordinates that define a piece, the typerepresenting which piece it is, the state which the piece first starts out in and finally the
color of the piece. These arrays are used to populate the fields of a tetris_piece struct. The
figure below shows the 7 pieces and what their different possible orientations are. I havealso attached the link for the figure from wikipedia.
8/7/2019 Parallel Tetris
9/19
8/7/2019 Parallel Tetris
10/19
There is an important point to mention. Before I was able to make the the arrays and
define the coordinates for the pieces I had to pick the regions on the screen where I was
going to display the players grid, the region where the next piece was displayed, theopponents grid and the region to display the players score. It was important to decide the
regions as the arrays defined the starting position of the piece when the actual game was
played. So it was important to know where the new generated piece was supposed to startdropping down from. Before I talk about the functions to change the position of the piece
there was one function I wanted to explain. As I was doing the drawing of pieces by
writing to memory, I wanted to minimize the number of writes to memory when theposition of the piece changed. For this I wrote a function drawpiece_tnext which takes in
the struct tetris_piece representing the piece at time t-1 and the piece at time t. This
function then compares the coordinates of both the pieces and erases only those squares
for the piece at time t-1 that dont overlap with the piece at time t + 1. Similarly it drawsonly those squares for the new piece which were not there in the piece at time t -1. This
helps speed up the process because if for example the piece was a square and you try and
rotate it, this function doesnt waste time first erasing the old piece and then redrwaing it
at the same place. It leaves the piece as it is.
Piece Movement Functions
Now that I had the functions for drawing the pieces and the definitions of the pieces I had
to write the movement functions. The basic ones were moveleft , moveright andmovedown which took in the current piece which is a tetris_piece struct and returns a
modified tetris_piece struct with the appropriate coordinates. For example, the
movedown function takes the original 4 coordinates which define the piece and add 20 tothe y value of each of the 4 coordinates. The reason you add 20 is because each square in
the grid is 20x20 pixels. So if you move down one square the y coordinate value
increases by 20. This similar logic is applied to moveleft and move right as well with thex coordinate values changing in that case. The tricky function was the rotate function aswhat happens depends on the previous state and the type of piece . This is where the
original tetris piece struct definition was very important as it simplifies this function. For
this function the input is again the current tetris_piece struct. The function checks whatthe type of the piece is and what the current state is. When I say current state I mean the
orientation of the piece as were seen in the tetris pieces picture earlier. To get the
rotations right I drew a grid and saw what the changes in the piece would be when itrotated and then modified the coordinates in the function appropriately. Similary I
updated the state of the piece every time it is rotated. The number of states depended on
the number of orientations the piece could have. To give an example of how this would
work, assume that the piece was a T and I defined this orientation to be state 0. Let thecoordinates defining this piece be (0,0) , (0,1) , (1,1) , (0,2). When this piece is rotated the
coordinates become (-1,-1),(0,1) ,(0,0) and (1,1) and the state becomes 1.As I remember
what I define as a particular state using careful coordinate updates I got the correctrotations for the game.
8/7/2019 Parallel Tetris
11/19
User Input
As I mentioned in an earlier section, the user input is through the PS/2 Keyboard. The
keys used by the game were up,down,left, right and spacebar.Left,right and down movethe piece by one small square in the grid in the appropriate direction. The spacebar key
makes the block go all the way down till it collides with a piece or the bottom. In thissection I will describe how I read a particular keyboard input and interpreted it. Therewere a couple of things I had to do in hardware and in software to make the keyboard
work. First I will describe the harware changes. As I had mentioned earlier I used a
Keyboard module which had been adapted from Digital Sysytems book I had mentioned
earlier. What this module does is that whenever a key is pressed it generates its ownclocks and shifts in the bits of the scan code which uniquely identify a key. Now this
scancode is what is eventually read by the NIOS and then interpreted as the appropriate
key. The problem with this for me was that if some one was pressing the keycontinuously then the bits being shifted in were continuously changing and the NIOS
would not be able to figure the key out as there would be a lot of bit combinations for the
same key. To get around this I used the scan ready clock generated by the keyboardmodule which goes high when it is shifting in a new set of bits. This for a particular key
press went high 4 times. So I kept a count register which counted till it was 4 and then
incremented a count1 register by 1 and reset count to 0. Now I assigned a register calledkeyscanreg the current scancode value it saw only when count1 changed. I then passed
this keyscanreg as the input into the NIOS. The reason this helped was because as the
keyscanreg only changed when a key was pressed its value would not constantly change
as new boits shifted in. This reduced the number of possible scan code values that couldbe read by the NIOS down to 3.The reason there are still combinations is because if a
button is presses a couple of times really fast or kep pressed this register does change fast
as well. In that case the NIOS might sample the value at different points of the bits being
shifted in giving rise to the different combinations. The software part was tricky toimplement as well. The things I had to get around was that since the scancode register
kept its value even if a key was not pressed again, I needed a way to interpret the keyonly when it had been pressed again since the last time. To get around this I used an input
into the NIOS called keycounter. I then kept a currentcount and previouscount variables.
The keycounter input is the count1 register I had mentioned earlier as that updates only
when a key is pressed. So in my game when I read a key I assign the currentcount thecount1 value and then compared it to previouscount. If it is greater than previouscount
then I interpret the key and take action. After that I set previouscount and currentcount
equal to each other. This is seen in tetris.c file linked later.The next problem I had wasthat I needed a way to be able to record multiple key presses or if a key was being held
down. To solve this problem I ran a loop that executed 30 times, and in each loopiteration I interpret the key and take the appropriate action. I ran the loop 30 times as thatnumber gave the user time for enough distinct key presses so as to make the game
realistic as you should ideally be able to move across the entire screen before moving
down even one block. The problem to doing this is that the scancode that is read in couldnow be more that one value as I was now accounting for a key having been held down or
it being pressed very fast. To get around this I used print statements in the loop at the
point where the currentcount is evaluated to be greater than the previouscount signalling
8/7/2019 Parallel Tetris
12/19
a key press. Printing the keyscan code as seen by the NIOS at this point enabled me to
see the different combinations for the keys the NIOS was seeing and I was able to use
this in the cases for deciphering a key. These were the tricks I used for user input andmaking the response quicker.
Serial PortI want to describe the how the serial port is used using by the NIOS before describing
how the game algorithm works. As mentioned earlier I added a RS-232 UART Module tothe NIOS using the SOPC builder. The UART basically raises an interrupt which I
connected to the IRQ 1 line. So everytime there is data that comes in I had a function
serialinterrupt that handle the data read. There are two things you do to make this work.First is registering the registering the IRQ 1 line and provding the function to go to for
handling the interrupt. This is done with the following code.
alt_irq_register(UART1_IRQ, (void*)&count, serialinterrupt );
The way you read data is with the following code
IORD_ALTERA_AVALON_UART_RXDATA(UART1_BASE). Writing to the serial
port is similar where the code is
IOWR_ALTERA_AVALON_UART_TXDATA(UART1_BASE, data). The tricky
part with the write when you do multiple writes is that you have to wait till the UART isready before you can write to the UART again. So I had to read the NIOS forums to
figure out that you have to wait till the ready to transmit bit in the status register of the
UART is 1 before wrting again. You can do this by looping and checking the bit andexiting the loop when it was 1. The code for that is given by
while((IORD_ALTERA_AVALON_UART_STATUS(UART1_BASE) & 0x040) !=
0x040){ ;}. The AND with 0x040 is because the status register bit 7 is the transmit readybit.
Playing Grid
One last thing to describe before getting into the actual game algorithm is to describe
how the mapping of the pieces is done to the actual grid and state of the game ismaintained. As mentioned earlier the playing grid is 20x10 small squares. As I picked the
region for the grid I knew the coordinates for the 4 corners of the 20x10 rectangle. What I
decided to do was to keep a 20x10 array in the actual game program to represent the grid,
where if the value in the array was 1, that meant that the piece was occupied and if it was0 then it was not. This is how I decided to maintain the state of the game. There was a
tricky part in terms of the the coordinates though. Now in the actual VGA monitor
coordinates the X coordinates is the horizontal line, but in the array the x represents thecolums. So I wrote a coor_change function given below.
struct coordinate coord_change(struct coordinate t9){
8/7/2019 Parallel Tetris
13/19
struct coordinate output ;
output.x = (t9.y - 50)/20;
output.y = (t9.x - 70)/20;
return output ;}
The reason the transformations work like this is because the top left square in the grid isthe [0][0] index in the array. As i started my playing region at x = 60 and y = 40, the
coordinates for the center of the first square is x = 70 and y = 50. Thats why this gives a
0,0 output if put into the function. Also notice that if the y coordinate of screencoordinates change the x coordinate of the array changes.This is because as I mentioned
earlier the y coordinates on the screen now represent the rows in the array. This is the
basic structure that was used for the grid and will be mentioned in the game algorithm
and collison detection.
Game algorithm
The basic game algorithm steps are listed below
1.Outer Loop checks if gover veriable is set. If it is game is over. If not then set current
piece to be the next piece. Set nextpiece to be one of the seven pieces chosen randomlyusing the rand() function.Display the current and the next piece.
2.Inner loop runs till a collison is detected. Will discuss how collison detection is done in
next section.Till collison is not detected read the user input using the loop discussed
earlier. Update the currentblock based on user input and what is an acceptable movebased on the grid occupancy. Once done with reading user input move the block down by
one small square. If there is a collison then update the state of the grid occupancy by
setting the value to 1 in the array for the indexes which the coordinates of the currentblock map to. So for example if coordinate is (70,50) set the array value at (0,0) to 1.
3.Once there is a collison and you exit the inner loop update the grid occupancy state by
clearing the lines which have been made. I did this by having a temp 20x10 array. I then
took the current grid state array and copied only those rows into the temp array whichwerent full. I then filled the rest of the rows with 0 values as that represents no blocks. I
kept track of the number of rows that I didnt copy over as those represented the lines
made by the player. This variable is what I used to update the current score of the player.
4.Now that I had an updated state, I had to check if the other player had made a line. I didthis by reading the input into the Nios which was othermadeline.I set this line in the
serialinterrupt function based on the data I receive from the other player. If the other
player did make a line (can be only 1,2,3 or 4) I create another grid array calledtempgrid2 where I fill the number of lines the other player made starting bottom up with
8/7/2019 Parallel Tetris
14/19
1 or 0 picked using the rand function.Then I fill the rest of the lines with the values from
the tempgrid1 which had the state of the system once the lines were cleared.
5. Now that I had the final updated state it was time to write memory and actually updatethe screen. The trick here is that you dont want to update memory locations which keep
their values after the update. So what I did was I looped through the tempgrid2 array andthe original state before the updates which is represented by tempgrid, and updated
tempgrid for only those locations that were different from tempgrid. At the same timethat I updated tempgrid, I also updated the SRAM.
6.Now came the tricky part. I had to send the state of the grid across to the other player.
So what I did was I created 36 variables where each variable represented 5 bits of the
current state of the grid. Remember that each value in the grid can only be a 1 or a 0. Inthe loop I basically OR'ed the variable with the bit and then shifted the variable left by
one. This way the variables held the bit values in the order they would be seen on the
grid. Once I had these variables I wrote each of these variables into the serial port using
the functions I had described earlier. As I knew what order I sent them in I knew whatorder I would receive in them. I then sent an 0xff byte which I will explain the
importance of later. Finally I sent the value of how many lines the player made which isvariable used to update the score.
7. The final step is checking whether the game was over. To do this I check if any of the
squares in the second row are filled. If they are then I exit the outer loop and the game is
over.
I will discuss how the data is interpreted when received by the serial port and how thescreen is drawn for the opponent in the VGA Controller section.
Collison Detection
In this section I will describe how I did collison detection at various stages of the game.The first time when this is needed is when updating the piece on a user input. What I do
on a user input is that I create a temp tetris_piece struct which gets the updated
coordinates based on the user input. If the coordinates lie outside the playing grid, or if
for the 4 coordinates the corresponding entry in the tempgrid array which holds thecurrent state of the grid is one then I dont update the piece otherwise I do. In the case
when spacebar is pressed the update of the coordinates is not as simple. For each different
x coordinate in the tetris piece I see what the highest y coordinate can be before there is a
filled piece in the grid. I then take the minimum of these y coordinates and update thecurrent piece's coordinates relative to that. These were all collison detections for the user
input updates. Once I am done reading the user input I have to check again for collisons ifthe piece was to move down by one square. First for the different x coordinates that
define the piece I find the maximum y coordinate. Then for each of the these x,y pair I
see in the tempgrid array if the square below is filled or not. If any of these are filled then
there is a collison. If not then I check if the max y coordinate of the piece is at the bottomof the grid. If it is then there is a collison. If neither of these conditions cause a collison
8/7/2019 Parallel Tetris
15/19
then I move the piece down by one square.The collison detection is made easier by
picking a proper way to represent the state of the grid and spreading out the collison
detection logic.
VGA Controller
The VGA controller I started out with was the one I referenced earlier in the report which
was one of Professor Land's DE2 hardware examples. There are changes I had to make to
the controller in order to make this project work. I will break down the changes based onthe section of the screen they affected. There are 4 distinct regions on the VGA monitor
for this project. The players grid, the grid to display the opponents status, the section
displaying the next piece and the section displaying the score of the player. The playersgrid and the next piece area were both drawn using the inputs from SRAM. This was
already implemented and I didnt have to change that logic. For the players score display I
knew what coordinate range corresponded to that area. So I used if statements in theVGA controller to know when the contrller was in the area. To display the score I wrote a
hexout module in the VGA controller which took in a 4 bit value and gave out a 7 bitoutput which represented which segments should be on similar to a seven segment
display. I used the iscoreofplayer input into the controller which was an output from theNIOS, and used the hexout module and logic to figure out what segments should be on
for different digits. I then used if statements and the outputs of the hexout module to draw
to a particular coordinate range for each segment which I picked.An example of this ifstatement is given below.
if((((H_Cont - X_START) > 280 ) && ((H_Cont - X_START) < 300)))
begin
if(((V_Cont - Y_START) == 150))
beginCur_Color_R 420 ) && ((V_Cont - Y_START) < 440)) &&
(((H_Cont - X_START) > 380 ) && ((H_Cont - X_START) < 400)) )
8/7/2019 Parallel Tetris
16/19
begin
Cur_Color_R
8/7/2019 Parallel Tetris
17/19
communication worked well. I was also able to successfully display the opponents grid
using the data received over the serial port successfully. The game can be played as a
single player and two player game. Though the game was fully functional there weresome glitches which I saw. For the VGA display which was done from the SRAM, there
are times when pieces leave behind a few pixels when they were updated. These specs
were cleared when another piece went over them and they did not interfere with the logic.I was not able to figure out why this happened as I made sure in the code that I cleared
the entire block when it moved. I could only think of the reason being that sometimes the
value was not successfully written to SRAM by the NIOS. Another glitch was theresponsiveness of the keyboard input. I got the delay between a key press to update down
to an acceptable level but it still could have been better. There are times when a key press
does not get acknowledged and requires another press for an update. This is because of
the way I did my user input where I was reading the scancode from the NIOS. I feel as ifdoing the user input that way leads to cases where an update is missed because the NIOS
might read a little early or too late. In most cases the update is fine but there are some
glitches as mentioned above.
Despite the glitches I believe that the project results met my expectations. I was able toachieve the functionality and goals that I had set when I started the project. Despite that
there are definitely things that I would do differently if I has the chance to redo the
project. The first thing I would do would be to have the user input be detected on aninterrupt and using a queue to service the requests. This would give better responsiveness
and accuracy improving the gaming experience. The second thing I would do would be to
make the entire VGA display using NIOS outputs and logic in the VGA controller. Thereason I would do this is because it would make the updates a lot quicker and would look
better.
This project was a lot of fun to work on and I learned a lot while working on it. Most ofthe C code for the NIOS and the VGA controller logic was written by me. The placeswhere I referenced or used designs I have linked the sources in the report. I will also
mark the sections in the code that I wrote to make it clearer.
IMAGES:
FIG:1
8/7/2019 Parallel Tetris
18/19
FIG:2
FIG:3 e-mail:[email protected]
FIG:4
8/7/2019 Parallel Tetris
19/19
Top Related