A Fully Commented turtleSpaces Logo Listing of PONG
When I was a kid I had a home PONG machine, one of those that was sold through a department store (in this case Sears) in the late 1970s, which my Dad bought from a garage sale for $5. It was black and white, and the paddles were controlled by knobs on the front of the unit, and the NES had come out by this point and so it wasn’t very enticing for the other kids in the neighbourhood, but my brother and I spent hours playing it anyway.
I’ve decided to take a different approach with this version of PONG, using a single turtle and a single thread taking a linear path through the code, rather than a multi-turtle approach because many programming languages do not have threads and it’s important to think about how you can accomplish things without them.
So, in this example, while the turtle acts as the ball, it also draws the paddles and the scoreboard as needed, and some tricks are used to smooth this over, the way you would in other single-threaded programming environments. Meanwhile, it also demonstrates the directional capabilities of the turtle, and how it operates in the turtleSpaces environment from its own perspective.
This project is divided into a number of user-defined procedures which could be worked on in groups in a classroom setting. There are a number of problems to be solved: moving the ‘ball’, bouncing it off of the walls and paddles, moving the paddles using the keyboard, updating the score. Pong is a well-known game and so its mechanics require little explanation. The key here is how do we do all of that with a single turtle?
Read on to find out!
SETABOUT |Use A and Z to control left paddle, K and M to control right paddle| CREATORS [melody] NEWTURTLE "myrtle TO pong ;this version of Pong uses a single turtle to re-create ;the game, employing a more traditional linear method ;rather than a multi-turtle, multi-threaded method. ;For a demonstration of the latter, see turtleSpaces ;Breakout, under the Examples menu in the web interpreter ;this is an extremely thoroughly commented listing, so ;please read through. It should be fairly straightforward ;to adapt this into a series of lessons for your class. ;There are many more comments than lines of code in this ;listing! Hopefully this will demonstrate to both you ;and your students the simplicity and power of Logo ;It might be helpful to first read the introduction to ;one of the Logo books available on the turtleSpaces ;website setup ;executes the setup procedure. Here we initialize containers ;(variables), draw the playfield, create the ball and paddles ;Note: medium-blue keywords indicate user-defined procedures forever [ checkkeys ;checks for and acts on player keypresses moveball ;moves the ball depending on a few factors checkball ;checks the ball position and acts if necessary ] ;square brackets indicate lists. In this case, we're ;providing the forever primitive (or command) with a list ;containing the three procedures we wish to execute ;'forever' (and also some comments, which get ignored) ;lists can generally be spaced out over multiple lines ;for readability. They also discard whitespace between ;items in the list. Logo is not a stickler for whitespace ;or formatting! ;there, that was easy, right? ;) ;now for the nitty-gritty... END TO setup reset ;reset the workspace. This returns all the turtles to ;their default states and positions resettime ;we're going to use the system time to 'speed up' the ball ;as gameplay goes on, and so we'll reset it now. The time ;is not reset by the reset primitive and so we need to ;declare it seperately penup ;no need to draw lines here! But for fun you can ;try to comment this line out and see what happens. ;It gets kind of messy, particularly because the turtle ;'ball' sneaks away and moves the paddles and updates ;the scoreboard while you aren't looking... noaudiowait ;don't delay while audio is played. For historical reasons, ;sound primtives (commands) such as toot and playnotes cause ;execution to pause while they are being played, but we can ;turn that off to make things proceed more smoothly makemodel ;create the turtle model (a voxel, or 3D pixel, to keep ;in the 1970s mood). This is a user-defined procedure arena ;draw the 'arena' or playfield. This is also a user- ;defined procedure make "keytimer 0 ;here we 'make' a 'container' used as a counter ;to limit the rate at which keys can be pressed ;and prevent 'flooding' of the game with too many ;keypresses make "movepaddle false ;used to indicate if the paddle has been moved ;and increase the speed of ball movement to allow the ;game to 'catch up' make "leftscore 0 make "rightscore 0 ;initialize the score containers and assign them ;values of 0 updatescore ;set up and draw the scoreboard. This is a user-defined ;procedure make "leftpaddle -20 drawpaddle "leftpaddle ;set the initial position of the left paddle ;and draw it, using the user-defined drawpaddle ;procedure, to which we pass the name of the paddle make "rightpaddle -20 drawpaddle "rightpaddle ;set up and draw the right paddle home ;reset the turtle's position to the home position ;and its orientation to the default showturtle ;show the turtle (ball) right 10 + (random 70) + ((random 3) * 90) ;set starting angle for the ball by turning ;the turtle to the right a random amount, ensuring ;that it doesn't send the ball straight up or down! ;That would get boring very quickly... ;Note: ;Round brackets () ensure the order of operations ;is correctly processed. turtleSpaces uses BEDMAS ;but processes equal-order operations right to ;left (like original Logo), which means that without ;brackets, the last 'random' primitive would be passed ;270 (3 * 90) which is not what we want! ;All right, we're all ready to go! ;let's return back to the main pong procedure... END TO makemodel setpremodel [setvectors [[0 1 0] [0 0 1] [1 0 0]]] ;setpremodel takes a list of commands, in this case ;we're providing it with a single command which itself ;takes a list of three lists, indicating orientation ;vectors (you don't need to worry about vectors for now). ;But you can see in this example how lists can get nested ;on a single line. setmodel [ setfillcolor yellow ;there are 16 default colors, and they each have an ;associated keyword, which just returns the index ;number of the color, which in the case of yellow is 13 setfillshade -5 ;you can set a shade for the color, which can be a ;value between -15 and 15. Excluding pure white and black ;there are 434 default colors, created using a combination ;of shade and color. But you can also define arbitrary ;colors using the definecolor primitive back 2.5 raise 2.5 ;raise elevates the turtle in the Z dimension, ;and this is the extent of the use of 3D movement ;primitives in this source code listing slideleft 2.5 voxel 5 ;because voxels are created to the front, ;right and beneath the turtle, we need to ;move the turtle before making the voxel if ;we want the voxel to be centered on the ;turtle's position ] ;The contents of lists can be spaced out across ;multiple lines for easier readability ;some explanation: ;The setvectors command inside the setpremodel command ;'fixes' the orientation of the voxel used as the turtle ;model regardless of the orientation of the turtle itself ;to appear more like a classic Pong pixel, while ;the setmodel primitive creates the voxel model itself. ;(For technical reasons, you can't assign a fixed ;orientation to a model inside a setmodel command) ;Comment out the makemodel command in the setup procedure ;by preceding it with a semicolon (like these comments are) ;to play instead with the actual turtle, and watch it as ;it changes direction when it bounces off the walls and ;paddles (THIS IS RECOMMENDED) ;This is because we use the turtle's current 'heading' ;or direction to determine how much to turn when we bounce ;the ball (or turtle) off of things. In reality, the turtle ;is constantly moving forward END TO arena ;let's draw the playfield: setpencolor lightblue setpos [0 -100] ;setpos takes a list of two values, X and Y. ;Remember, coordinates behind and to the left of ;the turtle's default position (at the center of ;the screen) are negative. ;Another primitive, setposition, takes a list ;of three values (X, Y and Z) with Z being positive ;above the turtle's default position, and negative ;below it. But because we're only working in two ;dimensions, we can use setpos here repeat 20 [mark 5 forward 5] ;draw dotted center line ;using 20 'dashes' or marks. ;the mark primitive uses the pen color ;to create a filled rectangle, like a marker setpc mediumblue ;setpc is shorthand for setpencolor. setfc is ;similarly shorthand for setfillcolor ;mediumblue returns 6, the palette index of ;the color that is a medium blue. Functions can ;be chained in intricate ways, passing their return ;values to other functions and finally commands ;(primitives that do not return a value). Logo is ;very flexible this way! setpos [-200 -100] right 90 ;turn the turtle to the right 90 degrees mark 400 ;mark the bottom line setpos [-200 100] mark 400 ;mark the top line ;That was simple! You could make it more complex ;if you like, but the retro aesthetic is groovy! END TO moveball if :movepaddle = false [ repeat 4 [forward 1] ;if the paddles haven't been moved since the last ;time we moved the ball, let's move it four turtle ;units forward if and xpos < 145 xpos > -145 [ make "move time / 1000 if :move < 20 [repeat int :move [forward 1]] else [repeat 20 [forward 1]] ] ;if the ball is presently well inside the area between ;the paddles, lets move the ball a bit more based on the ;amount of time elapsed since the round has started ;(to a maximum of 20 turtle units) ;This way, the ball will get faster and faster ;until someone misses it! ] else [ ;if the paddles HAVE been moved... if and xpos < 145 xpos > -145 [ ;and the ball is well inside the area between the paddles... repeat 10 [forward 1] ;move 10 turtle units instead of 4 so we can 'catch up' ;and reduce the 'lag' caused by moving the paddle make "move time / 1000 if :move < 20 [repeat int :move [fd 1]] [repeat 20 [fd 1]] ;also apply additional time-based 'speed' as above... ;fd is a shortcut for forward. Also if you supply a second ;list of instructions to an if statement, it will execute ;the second list if the comparison provided is false, ;similarly to the else primitive (although you can use ;else later in a procedure as it will take note of the ;result of the last comparison.) ] else [ repeat 4 [forward 1] ] ;but if the ball is closer to the paddles then let's move ;it only four, to provide a little more help catching it ;but also to ensure we detect the ball has 'hit' the ;paddle and doesn't accidentally pass through it, which ;will cause the player distress! make "movepaddle false ;finally, reset the movepaddle 'flag' to false ;(since we've dealt with it) ] ;and we have moved the ball! END TO checkball ;in this procedure, we check to see if the ball has ;passed over the boundary at the top and the bottom of ;the playfield, or if it has 'touched' the paddles, ;or if it has gone out of play, and we act accordingly if ypos > 100 [ ;if the ball has exceeded the top boundary of ;the playfield (the center of the playfield ;has x and y values of 0, which increase going ;upward and to the right, and decrease going downward ;and to the left): toot 600 10 ;make a 600 hz tone for 10/60ths of a second if heading > 180 [left 2 * (heading - 270)] ;the turtle's heading is a degree value increasing ;from zero in a clockwise direction (to the right) ;and so when the turtle is pointing right, its heading ;is 90, when pointing down 180, and when up 0. ;Here we check if the turtle is pointing to the left, ;(has a heading value greater than 180) and if so, we ;turn the turtle left twice the value of the heading ;minus 270 degrees, because we know the value of the ;heading is going to be greater than 270 degrees since ;the turtle is at the top wall. ;And so we turn double the angle between the turtle's ;current heading and the angle of the wall, thus causing ;the turtle to 'bounce' off of the wall else [right 2 * (90 - heading)] ;otherwise, we can assume the turtle is pointing to ;the right, and we do a similar calculation, instead ;subtracting the heading from 90 degrees, because ;we know the heading is going to be 90 degrees or less, ;based on the turtle striking the top boundary, and ;the turtle pointing to the right. forward 2 * (ypos - 100) ;because the ball can move more than one turtle unit ;at a time in order to make the gameplay speedy, we ;need to bring the ball back 'in bounds' so that we ;don't inadvertently read that the ball is out of bounds ;again before it has a chance to re-enter the playfield ;there is probably a more accurate way to do this, but ;simply doubling the distance the ball is out of bounds ;seems to be sufficient. But if the ball ever gets ;'stuck' out of play, you know what you need to fix! ] if ypos < -100 [ toot 600 10 if heading > 180 [right 2 * (90 + (180 - heading))] else [left 2 * (90 - (180 - heading))] forward 2 * abs (ypos - -100) ] ;this is similar to the above, except with the bottom ;boundary. Note that because the location of the bottom ;boundary is negative, we need to get the absolute (positive) ;value of the current turtle position minus the boundary ;(using the abs primitive) because the result of that ;calculation is otherwise negative ;now we check the paddles: if and xpos > 164 xpos < 171 [ ;if the ball is in the right paddle X 'zone': if and ypos > :rightpaddle ypos < (:rightpaddle + 40) [ ;AND if the ball is in the right paddle Y 'zone' (the ;area currently occupied by the paddle): toot 500 10 ;make a 500hz tone for 10/60ths of a second if heading > 90 [right 2 * (90 - (heading - 90))] else [left 2 * heading] ;this is similar to the top and bottom boundary ;calculations, except instead of changing based on ;if the turtle is facing right or left, here we ;do different calculations based on if the turtle is ;pointing downward or upward right -20 + (ypos - :rightpaddle) ;apply 'english' to the ball -- depending on the ;location on the paddle the ball is striking, turn ;the turtle to the left (which it does when a negative ;number is provided to the right primitive) or ;the right a related number of degrees. This allows ;the player to affect the trajectory of the ball ;and makes the game more interesting! forward 2 * (xpos - 164) ;make sure the ball is no longer in the 'strike' ;zone for the paddle, because otherwise it could ;be detected again and cause some strange behavior. ;There's probably a better way to do this, but ;this method seems to suffice ] ] ;Let's do this all again for the left paddle: if and xpos < -164 xpos > -171 [ ;if the ball is in the left paddle X 'zone': if and ypos > :leftpaddle ypos < (:leftpaddle + 40) [ ;and the ball is in the vertical area occupied by ;the paddle: toot 500 10 ;toot if heading > 270 [rt 2 * (360 - heading)] else [lt 2 * (90 - (270 - heading))] ;bounce left -20 + (ypos - :leftpaddle) ;apply 'english' forward 2 * (abs (xpos - -164)) ;get away from the paddle ] ] ;finally, we check if the ball has sailed past a player: if or xpos > 200 xpos < -200 [ ;if the ball's position exceeds either the left ;or right boundaries: toot 100 100 ;make a 100hz tone for 100/60ths of a second ;(1.66 seconds) if xpos > 200 [inc "leftscore] else [inc "rightscore] ;if the ball is past the right boundary, credit the left ;player with a point. Otherwise, credit the right player ;with a point. The inc primitive increases the value of ;the specified container by one. Note that it takes a ;quoted name, not a colon name for the container. updatescore ;update the score resettime ;we reset the time because we're using it to ;speed up the ball wait 100 ;wait 100/60ths of a second, for the toot to ;finish sounding if or :leftscore = 10 :rightscore = 10 [ ;if either player's score is now 10: hideturtle setpos [-90 -20] setfillcolor orange foreach "i |GAME OVER| [typeset :i wait 10] ;type out GAME OVER, but with a delay between ;each character, for dramatic effect audiowait playnotes "L3B3A3G3F3L6E3 ;play a little ditty, and wait for it to finish finish ;game over, man, game over! ] clean arena updatescore drawpaddle "leftpaddle drawpaddle "rightpaddle ;Because we're using a single turtle for this ;game, it is a good idea to 'clean' and redraw ;the game elements between rounds, because otherwise ;the 'turtle track' will gradually accumulate all of ;the ball movements and slow down its rendering over ;time. We want a speedy game so let's clean it up home showturtle right 10 + (random 70) + ((random 3) * 90) ;reposition, and randomly orient the ball ] ;we're done for now! END TO checkkeys ;in this procedure, we will check to see if a key ;has been pressed, and if so, act accordingly ;(by moving a paddle, if a paddle movement key ;has been pressed) inc "keytimer ;Because of key repeat, we want to ensure the player ;can't 'flood' the game with too many keypresses, and ;by using a simple counter, we can ensure this ;doesn't happen if and keyp :keytimer > 1 [ ;and so, we check to see if a key has been pressed ;(keyp) AND the :keytimer container's value is ;at least two. That means at least two ball movement ;cycles have to pass between paddle moves, keeping ;the game moving! make "keytimer 0 ;reset the keytimer container to 0 make "key readchar ;take a key from the keybuffer and put it into ;the key container clearchar ;clear the keyboard buffer. If we don't, key ;repeat (or a player hammering the key) can clag ;up the game if :key = "a [if :leftpaddle < 60 [ make "leftpaddle :leftpaddle + 20 drawpaddle "leftpaddle ] ] ;if the 'a' key is pressed, and the left paddle isn't ;already as high as it can go, increase its position ;by 20 turtle units and redraw it if :key = "z [if :leftpaddle > -100 [ make "leftpaddle :leftpaddle - 20 drawpaddle "leftpaddle ] ] ;if the 'z' key is pressed, and the left paddle isn't ;already as low as it can go, decrease its position by ;20 turtle units and redraw it if :key = "k [if :rightpaddle < 60 [ make "rightpaddle :rightpaddle + 20 drawpaddle "rightpaddle ] ] ;if the 'k' key is pressed, and the right paddle isn't ;already as high as it can go, increase its position by ;20 turtle units and redraw it if :key = "m [if :rightpaddle > -100 [ make "rightpaddle :rightpaddle - 20 drawpaddle "rightpaddle ] ] ;finally, if the 'm' key is pressed, and the right paddle ;isn't already as low as it can go, decrease its position ;by 20 turtle units and redraw it ] ;that's all for now! END TO updatescore ;this procedure updates the scoreboard hideturtle ;hide the ball norender ;suspend drawing the graphical elements while ;we update the scoreboard settypesize 20 ;set the size of the type, the graphical text if tagp "score [erasetag "score] ;if there is already a score 'tag', erase it. ;Tags mark areas of the 'turtle track' so that ;they can be copied, removed or used to create ;turtle models begintag "score ;create a score tag home ;reset the turtle's position and orientation setpos [-60 50] setfillcolor red typeset :leftscore ;type the left player's score setpos [40 50] setfillcolor green typeset :rightscore ;type the right player's score endtag ;close the score tag render ;resume rendering -- voila, the score is updated! END TO drawpaddle :paddle ;drawpaddle takes a parameter, which is 'passed' into ;the :paddle container, which exists only inside this ;procedure. Once we exit this procedure, it vanishes! ;We use the value contained in :paddle (the value we ;passed to drawpaddle) to decide which paddle to draw ;and reduce the amount of code we need to write (since ;we only need one procedure for both paddles) norender ;because we are going to erase the old paddle and ;then draw a new paddle, stop rendering the graphics ;so that it just seems like the paddle moved. ;It's magic! make "heading heading make "pos pos ;save the current position and heading of the turtle ;into two containers with similar names home ;reset the turtle's position and orientation if :paddle = "leftpaddle [ setfillcolor pink setpos {-170 :leftpaddle} ] ;if the paddle we're drawing is the left paddle, ;set the fill color to pink and move to the left paddle's ;position ;Curly-braces indicate a 'soft list', a list that is evaluated ;at the time of execution. Soft lists can contain :containers ;and functions which then get resolved to their values / results ;before being passed to the primitive they are attached to. ;This is how you can dynamically pass values to primitives ;that ordinarily take 'hard' [] lists ;Note that if you pass a string value in a soft list, you ;will need to precede it with a " or place it between pipes || else [ setfc cyan setpos {165 :rightpaddle} ] ;otherwise set the fill color to cyan and set the right ;paddle's position if tagp :paddle [erasetag :paddle] ;if there's already a paddle, erase its 'tag' from the ;turtle track. This erases the paddle, if it exists begintag :paddle ;create a new 'tag' with the name of the paddle, eg ;leftpaddle ;tags mark sections of the turtle track for manipulation ;later, such as to erase them, or use them to create a ;turtle model voxeloid 5 40 5 ;create the paddle voxel endtag ;close the tag setpos :pos setheading :heading ;reset the turtle's heading and position make "movepaddle true ;set the movepaddle container to true. This tells ;code in the moveball procedure that the paddle has ;moved and to make up for the time we lost doing it render ;start updating the graphic elements again ;abra cadabera! The paddle has moved! ;We've reached the end of this listing! ;Now, exactly how much Python code would you ;have had to have written in order to accomplish ;all this? Hint: a lot more! ;Thanks for reading! I hope this helps you on your ;journeys inside turtleSpaces. Bon Voyage! END NEWTURTLE "snappy NEWTURTLE "libby