A Starry turtleSpaces Logo Introduction Part Three: Starwarp Improvements

In the previous episode, we created a progressively-generated starfield we moved through with the camera turtle, creating a cool flying-through-space effect.

But it has a few issues we should address. Firstly, it is possible for a star to end up flying through the windscreen of our spaceship! Which is cool, but looks a bit strange. Second, as the program runs it piles up all of these stars behind us, which can slow everything down. We need to get rid of those. Finally, it would be pretty neat if we could have the stars ‘pop’ into view, so we’ll explore how we can do that.

Improvement One: Spaced Out Stars

So, firstly we want to ensure that stars aren’t placed too near to the center, where they could possibly fly through our windshield. Ideally we want to detect if we’re going to place a star within the narrow barrel our ship is flying through, and if so don’t place it there, place it somewhere else.

There are a few new tools we can use to accomplish this:

distance – takes two lists of coordinates, eg [x1 y1 z1] [x2 y2 z2], and returns the distance between them. This will be useful to us, because we are going to provide the prospective new spot position as one list, and {0 0 zpos} as the second list, giving us the distance between the new spot and the XY center of the space at Myrtle’s current Z position.

dountil – repeats a list of commands until it gets the desired results. In this case, we’re going to want to have dountil pick a random position, and then use the distance function to check if that position is outside of our no-go range. dountil executes the list of instructions it is provided before checking the condition it needs to stop executing, while its cousin until checks first.

So, to ensure we don’t place a star within that ‘barrel’, instead of the existing setposition command, we do the following:

dountil 100 < distance position {0 0 zpos} [
  setposition {-1500 + random 3000 -500 + random 1000 zpos}
]

So, until 100 is less than the distance between the turtle’s position and the XY center of the turtle’s current Z position, keep choosing a new position — and choose a new position before doing the first check.

Note: because of the way Logo’s parser works, if you do a comparison operation (<, >, = etc) where one side is a single parameter (eg a number) you’re best to put that FIRST and the complex parameter second.

Why? Because Logo collects things up right to left, and while Logo will evaluate the stuff to the right of the operator correctly, passing that to the operator, the parser will give the operator the first ‘complete’ thing it sees to the left of it, which if you switched things around would be {0 0 zpos} and is not what we want!

So to solve this you would need to put round brackets () around distance position {0 0 zpos} to ensure that Logo evaluated and gave < what we really wanted to give it. It’s easier in this case just to put the single parameter on the left.

This solves our problem! Stars will keep out of our way. However, this method also moves the turtle every time we try a new position, and all of these false jumps will stay in the turtle’s ‘turtle track’ or list of things the turtle has done.

There are a few methods we could use to stop this from happening, but this one is probably the simplest:

dountil 100 > distance :position {0 0 zpos) [
  make "position {-1500 + random 3000 -500 + random 1000 zpos}
]
setposition :position

Rather than set the position every time we try, we make a container (variable) called position containing the prospective coordinates, and test that instead. Then, once we have a good set of coordinates, we set that :position using the setposition command.

A colon before a word indicates to the parser that it is meant to pass the contents of a container with that name to the next command or function in the chain.

The colon is shorthand for thing, which returns the value of the container named passed to it. So, for example, you could have used setposition thing “position instead. Note that the name of the variable is preceded by a quote, not a colon. If you used a colon, thing would return the value of the container with the name CONTAINED inside of the container you referenced with the colon!

The mind boggles, doesn’t it?

This is also why you generally make “container rather than make :container — if you did the second, you would make a container with the name contained inside of the container you referenced, which does have practical applications but can be a bit confusing at first.

For now, just remember you make with a quote, retrieve with a colon.

Okay, moving on…

 

Improvement Two: Galactic Janitorial

This procedure creates a lot of stars. Like, a lot. While you can have a lot of things in turtleSpaces (so many things!) they can start to gum up the works if they get to extreme numbers. A computer can only remember so much you know! And so, we should clean out the stars behind us, because they don’t matter to us anymore anyway.

But the stars are part of Myrtle’s ‘turtle track’, and so we can’t just clean out some of them, can we? There are the clean and clearscreen commands but they get rid of everything!

Never fear, tag is here!

tag allows you to wrap one or more turtle track entries so that you can reference them later, to replace, copy or delete them. We’re going to put tags around stars so we can delete them. We do this with begintag and endtag.

We need to give each tag a name, and so we need to give begintag a name to use. endtag doesn’t take a name, since we can only close the last opened tag. If you create a tag within a tag, that tag has to be closed first, before you can close the tag above it.

What we’re going to do is every 2000 stars, we’re going to close off the existing tag (the stars still  in front of the camera turtle), then create a new tag for the next batch of stars, and erase the tag that came before the one that we just closed off (the stars now behind the camera turtle). It’s a real slight-of-hand magic trick!

To set up our tags, first we have to add the following before the forever loop:

  begintag 0
  endtag
  ;dummy 'first' tag (numbered 0)
  make "tag 1
  ;create tag container
  begintag :tag
  ;create second tag using the tag
  ;container value

Because we’re erasing the tag before the previous tag, we need to create a dummy 0 tag to erase when we get started. Then we create a tag container, which contains the value (number) of the current tag, and then we create a tag with a name of that value (1).

We’re all ready to go! Now, inside of the forever loop, we need to add:

    if divisorp 2000 loopcount [
      ;every 2000 loops:
      endtag
      erasetag :tag - 1
      ;erase the tag BEFORE the tag we just closed
      inc "tag
      ;increment the tag container
      begintag :tag
      ;start a new tag
    ]

if is sort of like dountil, except that it checks if the condition is true first and then executes the list of instructions provided to it, but it only does it once and only if the condition is true.

divisorp is a boolean, it returns only true or false. Because of booleans, unlike in other programming languages if does not require a comparison operator. Comparison operators (=, >, < etc) are themselves boolean operators — they return either true or false to if, until, dountil or whatever other command needs a boolean parameter passed to them.

divisorp returns true if the first value passed to it divides equally into the second value. divisorp  in this case returns true if 2000 divides equally into loopcount, which is the total count of the number of loops executed  — for example, the number of times we’ve been through the forever loop.

So every 2000 times through the forever loop, divisorp returns a true value and that causes if to execute the contents of the list. Which is:

endtag – end the current tag

erasetag :tag – 1 – erase the tag before the tag we just closed

inc “tag – increment the tag counter

begintag :tag – start a new tag

And that’s all there is to it. The janitor will come every so often and sweep out those old stars, keeping things running smoothly!

 

Improvement Three: View Screen On

I’m kind of torn on which is cooler, having the galaxy wink into existence around us or approaching it as if we’ve arrived from the intergalactic void, but if you want to try the winking-into-existence option, here’s how to do it:

First, before the forever loop, we put in a norender command. This stops turtleSpaces from updating the ‘render’, or representation of items in 3D space.

Second, inside the forever loop, before everything else, we need to put the following:

    if loopcount = 1500 [
      render
      print |Engage!|
    ]

which as you may remember from the previous section, after 1500 loops will cause if to execute the render command, which turns rendering back on. This ensures there are stars for us to see before we start to see them.

That’s all there is to it!
Bonus Improvement: Spinning Through Space

As a final bonus improvement, after the cam:forward command, insert the following:

cam:rollright 1/10

This will simulate the gentle roll of the spacecraft as it travels through the stars, causing the stars to spin slightly around the spacecraft.

Congratulations! You’ve graduated from starwarp academy! Good job. Welcome to turtleSpaces.

 

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

 

One-a-Day: Space Tube Ride using TUBE and TUBEARC

Myrtle creates a 3D space tube ride for her to fly through…

Sometimes Myrtle needs to take a break and have a little fun! One of the things she can do is make a space tube and fly through it, using the TUBE and TUBEARC primitives:

TUBE makes a straight tube, and takes the parameters <radius> <depth> and <sides>. So, tube 20 10 10 makes a tube with a radius of 20 turtle units, a depth of 10 turtle units and 10 sides.

TUBEARC creates a curved tube, and takes the parameters <thickness> (the radius of the tube itself) <radius> (the radius of the arc itself) <sides> <totalsegments> and <arcsegments> – the last two are the total segments that would make up a complete torus, or donut, and the number of segments we actually want to create. So values of 20 and 10 will create a half-donut: tubearc 10 20 20 20 10

There are four procedures to our space tube program: createcourse, drawcourse, followcourse and spacetube.

createcourse generates the data that represents the tube’s course. It creates a course made up of 100 segments, represented by a word made up of a series of numbers between 0 and 4, where 0 represents a straight section of tube, and 1 to 4 represent four directions of turn. We use dountil to ensure we don’t repeat the same direction of turn more than once and end up looping back on ourself. Although, createcourse is not perfect and it is still possible for the course to cross over itself – but it’s a good thing tube walls aren’t solid!

drawcourse creates the graphical representation of the contents of the :course container we generated using createcourse. We use the twosided primitive to generate reflective “normals” on both sides of the shapes we generate from that point forward, and we use randfc to set a random ‘fill color’ or shape color for each segment as we create them.

We use foreach to step through each segment listed in the :course container, creating either a tube or a tubearc where appropriate.

While tubes are created directly under Myrtle, tubearcs are created around her, and so this means we need to do a bit of calisthenics to position the tubearcs where they need to go. Follow them through one command at a time to see how they work! We wait 2 / 60ths of a second between each segment creation so we can follow it.

followcourse causes Myrtle to fly through the created course, once again iterating through the :course container using foreach. fluid causes Myrtle to move forward smoothly, and to turn through the arcs we use a repeat. sleep is another form of wait, which takes milliseconds as a parameter instead of wait‘s 60ths of a second.

setpremodel allows us to ‘prepend’ commands to the turtle’s model, allowing us to make Myrtle look like she’s turning the appropriate way as she moves through the course.

Finally, spacetube puts it all together and is the procedure we execute to create the maze and fly through it. spacetube performs a reset, turns off the text layer, tells Snappy (the camera turtle) to pull away from Myrtle 1000 turtle units, tells Snappy to ‘follow’ Myrtle (turn to face her when she disappears from his view), then executes the createcourse procedure, the drawcourse procedure, sets the view (camera) turtle to Myrtle, turns Myrtle’s ‘light’ on (so we can see properly inside the tubes), turns Snappy’s light off, sets the camera ‘view point’ of Myrtle a bit up and behind her, and then executes followcourse.

TO spacetube
  reset
  fullscreen
  snappy:pullout 1000
  snappy:follow "myrtle
  createcourse
  drawcourse
  setview "myrtle
  setlight 1
  snappy:setlight 0
  setviewpoint [0 10 -30]
  followcourse
END

TO createcourse
  make "course empty
  make "last 5
  repeat 100 [
    
    dountil :last != :segment [make "segment random 5]
    
    make "course word :course :segment
    if :segment != 0 [make "last :segment]
  ]
END

TO drawcourse
  nofluid
  cs
  pu twosided
  foreach "segment :course [
    randfc
    if :segment = 0 [tube 50 200 20 lo 200]
    else [rt :segment * 90
      dn 90 rr 90 sl 100 tubearc 50 100 20 20 5 fd 100 rr 90 lt 180 + :segment * 90]
    wait 2
  ]
  home
END

TO followcourse
  dn 90
  fluid
  foreach "segment :course [
    if :segment = 0 [fd 200]
    if :segment = 1 [setpremodel [rt 22.5] repeat 90 [fd 1.75 rt 1 sleep 1] setpremodel []]
    if :segment = 2 [setpremodel [dn 22.5] repeat 90 [fd 1.75 dn 1 sleep 1] setpremodel []]
    if :segment = 3 [setpremodel [lt 22.5] repeat 90 [fd 1.75 lt 1 sleep 1] setpremodel []]
    if :segment = 4 [setpremodel [up 22.5] repeat 90 [fd 1.75 up 1 sleep 1] setpremodel []]
  ]
END