Example: Tetris written in turtleSpaces Logo
What do Steve Wozniak and George H.W. Bush have in common? They’ve both been seriously into Tetris! But who can blame them? The object of the game (as if you didn’t know) is to complete horizontal lines using falling shapes of various configurations. When you finish a line, it disappears, causing the rest of the blocks to fall down a line. However, if you stack up shapes to the point they overflow the top of the playfield: Game Over. When you finish a certain number of lines, the level ends… and in the next, the shapes fall faster, and you need to complete more lines! The insanity never ends.
In an era where games were becoming increasingly more complex, the simplicity of Tetris was seen as a breath of fresh air. Tetris would inspire a number of other “falling block puzzle games” such as Sega’s colour-matching Columns, and the three-dimensional Welltris. But Tetris would always remain king (tsar?) of the arcade puzzle game world, with sequels, clones and variations being released for virtually every console, computer, and operating system worldwide.
A little history
Alexey Pajitnov’s Tetris – early unauthorised versions of which, such as Spectrum Holobyte’s rendition described above, began appearing on home computers in 1987 – spawned a whole new generation of shape-based puzzle games.
Holobyte’s version took next year’s CES by a storm, and garnered the attention of the Soviet government, who held the rights to the game. They sold the arcade rights to Atari and the console rights to Nintendo.
Nintendo released Tetris on both its Nintendo Entertainment System and on the Game Boy, the latter as a pack-in with the portable console. The inclusion of Tetris arguably made the Game Boy a success –the game was perfect for smaller screen sizes, and was very addictive, spawning a whole generation of Tetris ‘junkies’.
Nintendo’s version of Tetris for the NES was criticised for not having a two-player mode; however, Atari Games, perhaps mistakenly believing their arcade licensing gave them the right to release a console version, came out with their own Nintendo Tetris game through their Tengen subsidiary (created after the consumer rights to the Atari name were sold to Jack Tramiel, see Point & Click) which featured head-to-head play.
Nintendo sued, and Tengen was eventually forced to withdraw its cartridge from sale, after selling around 100,000 copies. They are collectibles.
All right, so let’s take a look at the listing. The first thing you’ll notice is that it’s very monolithic, there’s only one procedure! That’s okay, though, in this context. While Logo is very versatile, sometimes aspects of that versatility such as the use of multiple procedures are unneeded.
You can also view the example inside of turtleSpaces webLogo by clicking File -> Browse Published… after it finishes loading and selecting the Tetris project.
In this example, we handle most of the structure using dountil and switch / case — dountil loops its contents until a condition is true, while switch takes a value from a container (variable) and then case compares the value it is given with the switch, and executes its list if that condition is true.
The game procedure begins with some basic initialization, such as creating the table we will use to note the finishing positions of the pieces, draws the border around the playfield and then proceeds into the main ‘run loop’, the dountil :gamover which loops until the gameover container is set to true.
Inside the dountil loop we choose the next Tetris piece to play, set a random fill color, set the starting position for the piece based on its characteristics (we don’t want it overlapping the edges or starting ‘over the top’) and ensure that the new piece doesn’t overlap an existing piece (if so, game over!)
If it doesn’t, we continue into the dountil :dropped loop, which runs until the piece has fully ‘dropped’ into its final position. We use a switch statement and case statements to run the logic relevant to the current piece. For readability I decided to make each orientation of a piece its own piece — while this means there are a lot more ‘pieces’ than if I had handled variants (rotations) using the same chunks of code, these are simpler to understand than they would have been had they had all of these conditionals in them dependent on the orientation of the piece.
Also, you can hand each orientation of a new piece off to a student to complete, which could make for an interesting group project. A number of pieces have not been implemented in this example, which could be implemented by your students, based off of the pieces that have been implemented.
Inside each piece subroutine we check for a keypress, then act if there was a keypress, checking to see if the action (moving left, right, or ‘rotating’ (switching to another orientation)) would collide with an existing piece or the edges before performing the action.
Then we draw the piece, and delay. There is a ‘drop key’ that sets the delay to zero for the remainder of the piece’s time in play. Then we see if we’ve ‘landed’ either on another piece or on the bottom, by checking the appropriate cells in the table. If so, we set the :dropped container to true, so that we will ‘drop out’ of the dountil loop, and we set our current piece’s occupied cells in the table. We only bother to set these when the piece has finished moving because there is no point in setting them while it’s dropping.
If we haven’t landed, we turn off rendering (so things don’t flicker) and ‘backtrack’ the necessary number of steps to ‘undo’ the drawing of the piece. Since turtleSpaces graphics are made out of 3D shapes, we can’t just erase a particular area, we have to erase or backtrack part of the turtle track — the list of objects the turtle has created.
You could also use tags to do the same thing, wrapping the piece in a tag, and then erasing the tag. turtleSpaces is versatile!
If the current piece has ‘dropped’ we then check to see if we’ve filled in any lines. To do this we sequentially run horizontally through each row in the table, checking each column, and adding to a counter each time a column has a value in it. If we count up to 10, we have a full row, and we erase it by copying the contents of all of the above rows down one row, clearing the top row when we are done. We add one to a score counter, and inform the user of their success.
The game then continues until the :gameover dountil condition is met, at which point it terminates.
TO game reset penup hideturtle ;create table to hold tetris piece placement information: newtable "board [10 21] ;draw playfield border: setpos [-60 -110] voxeloid 10 210 10 voxeloid 120 10 10 setpos [50 -110] voxeloid 10 210 10 make "gameover false make "score 0 dountil :gameover [ ;pick random piece: make "piece pick [square hline vline hzed vzed hess vess] ;the logic is easier to implement if each orientation of a shape ;is its own piece. While this means implementing a lot of pieces ;to make a full Tetris game, it avoids cumbersome if statements ;TODO: tee0 tee90 tee180 tee270 -- T shapes ;TODO: jay0 jay90 jay180 jay270 -- J shapes ;TODO: elle0 elle90 elle180 elle270 -- L shapes ;MAYBE: crowbar corkscrew rightangle cshape? show :piece randomfillcolor ;choose random starting placement based on the piece. ;We need to make sure the piece is fully in the playfield ;and doesn't overlap any edges: if :piece = "square [make "y 1 make "x 1 + random 9] if :piece = "hline [make "y 0 make "x 1 + random 7] if :piece = "vline [make "y 3 make "x 1 + random 10] if :piece = "hzed [make "y 0 make "x 1 + random 8] if :piece = "vzed [make "y 2 make "x 1 + random 8] if :piece = "hess [make "y 1 make "x 1 + random 8] if :piece = "vess [make "y 2 make "x 2 + random 9] ;check if the area selected for the new piece is already ;occupied by another piece, if so then game over: if :piece = "square [ if (or not emptyp cell "board {:x :y} not emptyp cell "board {:x + 1 :y} not emptyp cell "board {:x :y + 1} not emptyp cell "board {:x + 1 :y + 1}) [make "gameover true]] if :piece = "hline [ if (or not emptyp cell "board {:x :y + 1} not emptyp cell "board {:x + 1 :y + 1} not emptyp cell "board {:x + 2 :y + 1} not emptyp cell "board {:x + 3 :y + 1}) [make "gameover true]] if :piece = "vline [ if (or not emptyp cell "board {:x :y + 1} not emptyp cell "board {:x :y} not emptyp cell "board {:x :y - 1} not emptyp cell "board {:x :y - 2}) [make "gameover true]] if :piece = "hzed [ if (or not emptyp cell "board {:x :y + 1} not emptyp cell "board {:x + 1 :y + 1} not emptyp cell "board {:x + 1 :y + 2} not emptyp cell "board {:x + 2 :y + 2}) [make "gameover true]] if :piece = "vzed [ if (or not emptyp cell "board {:x :y + 1} not emptyp cell "board {:x :y} not emptyp cell "board {:x + 1 :y} not emptyp cell "board {:x + 1 :y - 1}) [make "gameover true]] if :piece = "hess [ if (or not emptyp cell "board {:x :y + 1} not emptyp cell "board {:x + 1 :y + 1} not emptyp cell "board {:x + 1 :y} not emptyp cell "board {:x + 2 :y}) [make "gameover true]] if :piece = "vess [ if (or not emptyp cell "board {:x :y + 1} not emptyp cell "board {:x :y} not emptyp cell "board {:x - 1 :y} not emptyp cell "board {:x - 1 :y - 1}) [make "gameover true]] make "dropped false ;boolean to indicate the piece has 'landed' make "speed 10 ;the speed at which the pieces 'fall' dountil :dropped [ ;repeat the following until :dropped is true switch "piece ;based on the value of the "piece container, execute one ;of the following case statements: case "square [ make "y :y + 1 ;increment "y if keyp [ make "key readchar if :key = "j [ if :x > 1 [ if (and emptyp cell "board {:x - 1 :y} emptyp cell "board {:x - 1 :y - 1}) [dec "x]]] if :key = "k [ if :x < 9 [ if (and emptyp cell "board {:x + 2 :y} emptyp cell "board {:x + 2 :y - 1}) [inc "x]]] if :key = "m [make "speed 0] clearchar ] ;we check to see if a key has been pressed, and ;if it has, then we see if it's one of the keys ;we respond to, and then we respond to them IF ;moving the piece will not collide with already ;existing pieces. m sets the speed to 0 and causes ;it to drop as quickly as possible ;clearchar clears the keyboard buffer sety 100 - :y * 10 setx -60 + :x * 10 voxeloid 20 20 10 ;sets the turtle's position and draws the shape if :speed > 0 [trackrender] ;don't render if the shape is 'dropping' ;(eg we pressed the m key) wait :speed ;delay in 60ths of a second, so 10 is 1/6th of a second ;check to see if we've landed on another piece, ;or hit the bottom, and then enter the piece position ;into the table: if (or :y = 20 not emptyp cell "board {:x :y + 1} not emptyp cell "board {:x + 1 :y + 1}) [ setcell "board {:x :y} fc setcell "board {:x + 1 :y} fc setcell "board {:x :y - 1} fc setcell "board {:x + 1 :y - 1} fc make "dropped true ;set dropped to true so we continue to the ;line check routine ] ;if we haven't 'landed' then turn off rendering, ;and 'undo' the drawing of the piece in preparation ;for the next move: else [notrackrender backtrack] ] ;that's it for this piece, on to the next! ;As the pieces get more complex, so does the logic ;needed to position and move them case "hline [ make "y :y + 1 if keyp [ make "key readchar if :key = "j [ if :x > 1 [ if emptyp cell "board {:x - 1 :y} [dec "x]]] if :key = "k [ if :x < 7 [ if emptyp cell "board {:x + 4 :y} [inc "x]]] if :key = "i [ if :y > 3 [make "piece "vline]] if :key = "m [make "speed 0] clearchar ] sety 100 - :y * 10 setx -60 + :x * 10 voxeloid 40 10 10 if :speed > 0 [trackrender] wait :speed if (or :y = 20 not emptyp cell "board {:x :y + 1} not emptyp cell "board {:x + 1 :y + 1} not emptyp cell "board {:x + 2 :y + 1} not emptyp cell "board {:x + 3 :y + 1}) [ setcell "board {:x :y} fc setcell "board {:x + 1 :y} fc setcell "board {:x + 2 :y} fc setcell "board {:x + 3 :y} fc make "dropped true ] else [notrackrender bt] ] case "vline [ make "y :y + 1 if keyp [ make "key readchar if :key = "j [ if :x > 1 [ if (and emptyp cell "board {:x - 1 :y} emptyp cell "board {:x - 1 :y - 1} emptyp cell "board {:x - 1 :y - 2} emptyp cell "board {:x - 1 :y - 3}) [dec "x]]] if :key = "k [ if :x < 10 [ if (and emptyp cell "board {:x + 1 :y} emptyp cell "board {:x + 1 :y - 1} emptyp cell "board {:x + 1 :y - 2} emptyp cell "board {:x + 1 :y - 3}) [inc "x]]] if :key = "i [ if :x < 8 [ if (and emptyp cell "board {:x + 1 :y + 1} emptyp cell "board {:x + 2 :y + 1} emptyp cell "board {:x + 3 :y + 1}) [make "piece "hline]]] if :key = "m [make "speed 0] clearchar ] sety 100 - :y * 10 setx -60 + :x * 10 voxeloid 10 40 10 if :speed > 0 [trackrender] wait :speed if (or :y = 20 not emptyp cell "board {:x :y + 1}) [ setcell "board {:x :y} fc setcell "board {:x :y - 1} fc setcell "board {:x :y - 2} fc setcell "board {:x :y - 3} fc make "dropped true ] else [notrackrender bt] ] case "hzed [ make "y :y + 1 if keyp [ make "key readchar if :key = "j [ if :x > 1 [ if (and emptyp cell "board {:x - 1 :y} emptyp cell "board {:x :y + 1}) [dec "x]]] if :key = "k [ if :x < 8 [if (and emptyp cell "board {:x + 2 :y} emptyp cell "board {:x + 3 :y + 1}) [inc "x]]] if :key = "i [if :x > 1 [if (and emptyp cell "board {:x + 1 :y} emptyp cell "board {:x + 1 :y - 1}) [make "piece "vzed]]] if :key = "m [make "speed 0] clearchar ] sety 100 - :y * 10 setx -60 + :x * 10 voxeloid 20 10 10 bk 10 sr 10 voxeloid 20 10 10 if :speed > 0 [trackrender] wait :speed if (or :y = 19 not emptyp cell "board {:x :y + 1} not emptyp cell "board {:x + 1 :y + 2} not emptyp cell "board {:x + 2 :y + 2}) [ setcell "board {:x :y} fc setcell "board {:x + 1 :y} fc setcell "board {:x + 1 :y + 1} fc setcell "board {:x + 2 :y + 1} fc make "dropped true ] else [notrackrender repeat 4 [bt]] ] case "vzed [ make "y :y + 1 if keyp [ make "key readchar if :key = "j [ if :x > 1 [ if (and emptyp cell "board {:x - 1 :y} emptyp cell "board {:x - 1 :y - 1} emptyp cell "board {:x :y - 2}) [dec "x]]] if :key = "k [ if :x < 9 [ if (and emptyp cell "board {:x + 1 :y} emptyp cell "board {:x + 2 :y - 1} emptyp cell "board {:x + 2 :y - 2}) [inc "x]]] if :key = "i [ if (and :y > 2 :x < 9 :y < 19) [ if (and emptyp cell "board {:x + 1 :y + 1} emptyp cell "board {:x + 2 :y + 2} emptyp cell "board {:x + 3 :y + 2}) [ make "piece "hzed]]] if :key = "m [make "speed 0] clearchar ] sety 100 - :y * 10 setx -60 + :x * 10 voxeloid 10 20 10 fd 10 sr 10 voxeloid 10 20 10 if :speed > 0 [trackrender] wait :speed if (or :y = 20 not emptyp cell "board {:x :y + 1} not emptyp cell "board {:x + 1 :y}) [ setcell "board {:x :y} fc setcell "board {:x :y - 1} fc setcell "board {:x + 1 :y - 1} fc setcell "board {:x + 1 :y - 2} fc make "dropped true ] else [notrackrender repeat 4 [bt]] ] case "hess [ make "y :y + 1 if keyp [ make "key readchar if :key = "j [ if :x > 1 [ if (and emptyp cell "board {:x - 1 :y} emptyp cell "board {:x :y - 1}) [dec "x]]] if :key = "k [ if :x < 8 [ if (and emptyp cell "board {:x + 2 :y} emptyp cell "board {:x + 3 :y - 1}) [inc "x]]] if :key = "i [ if (and :y > 2 :x > 1) [ if (and emptyp cell "board {:x :y} emptyp cell "board {:x - 1 :y} emptyp cell "board {:x - 1 :y - 1}) [ make "piece "vess]]] if :key = "m [make "speed 0] clearchar ] sety 100 - :y * 10 setx -60 + :x * 10 voxeloid 20 10 10 fd 10 sr 10 voxeloid 20 10 10 if :speed > 0 [trackrender] wait :speed if (or :y = 20 not emptyp cell "board {:x :y + 1} not emptyp cell "board {:x + 1 :y + 1} not emptyp cell "board {:x + 2 :y}) [ setcell "board {:x :y} fc setcell "board {:x + 1 :y} fc setcell "board {:x + 1 :y - 1} fc setcell "board {:x + 2 :y - 1} fc make "dropped true ] else [notrackrender repeat 4 [bt]] ] case "vess [ make "y :y + 1 if keyp [ make "key readchar if :key = "j [ if :x > 2 [ if (and emptyp cell "board {:x - 1 :y} emptyp cell "board {:x - 2 :y - 1} emptyp cell "board {:x - 2 :y - 2}) [dec "x]]] if :key = "k [ if :x < 10 [ if (and emptyp cell "board {:x + 1 :y} emptyp cell "board {:x + 1 :y - 1} emptyp cell "board {:x :y - 2}) [inc "x]]] if :key = "i [ if (and :y > 2 :x < 9 not :y = 20) [ if (and emptyp cell "board {:x + 1 :y + 1} emptyp cell "board {:x + 2 :y - 1}) [ make "piece "hess]]] if :key = "m [make "speed 0] clearchar ] sety 100 - :y * 10 setx -60 + :x * 10 voxeloid 10 20 10 fd 10 sl 10 voxeloid 10 20 10 if :speed > 0 [trackrender] wait :speed if (or :y = 20 not emptyp cell "board {:x :y + 1} not emptyp cell "board {:x - 1 :y}) [ setcell "board {:x :y} fc setcell "board {:x :y - 1} fc setcell "board {:x - 1 :y - 1} fc setcell "board {:x - 1 :y - 2} fc make "dropped true ] else [notrackrender repeat 4 [bt]] ] ] ;if the game is over, don't bother checking for lines: if :gameover [go "gameover] ;the following code checks to see if we've made any lines: trackrender ;turns on rendering the turtle track ;we're going to check every column of every row. Every time ;we get a 'hit' checking a column, we add up a counter, called ;"count. If :count hits 10, then we've made a line: repeat 20 [ make "count 0 repeat 10 [ if not emptyp cell "board {repcount repabove 1} [inc "count] ] ;repabove 1 returns the loop value of the repeat loop above ;the current repeat loop. repabove 2 returns the loop count ;above that, and so on ;if we've made a line, we need to shuffle every line above ;it down one, in order to remove it: if :count = 10 [ inc "score (print "Score: :score) ;increase the player's score by one and display the score repeat repcount - 1 [ repeat 10 [ (setcell "board {repcount (repabove 2) - (repabove 1) + 1} cell "board {repcount (repabove 2) - (repabove 1)}) ] ] ;we need to remember to clear the line at the top: repeat 10 [setcell "board {repcount 1} empty] ;now we need to redraw the playfield to match the table: notrackrender cs setfc 7 setpos [-60 -110] voxeloid 10 210 10 voxeloid 120 10 10 setpos [50 -110] voxeloid 10 210 10 ;draw the border ;we iterate through the table and if there's a color ;recorded in the specific cell, we create a matching ;voxel: repeat 20 [ repeat 10 [ setx -60 + 10 * repcount sety 100 - 10 * repabove 1 if not emptyp cell "board {repcount repabove 1} [ setfc cell "board {repcount repabove 1} voxel 10] ] ] trackrender ] ] label "gameover ] ;game over, man... game over! print |Game Over!| END