Wordle, a cross between Mastermind and Hangman, seems to be the rage these days. Also, implementing it in as few lines as possible. In Logo we get to cheat a little about the second bit, as we can stack multiple commands on the same line. But we still need to keep things within a reasonable line length.
We’re happy to say we succeeded in our efforts, and even expanded on the model somewhat, adding progressively longer words and hints, among other things. This is a pretty good example of string and list management in Logo, which is what it was initially created for, even before the turtle!
NEWTURTLE "myrtle
TO floaty
;here is yet another example of a relatively complex
;game that can be rather simply implemented in Logo:
;a flappy bird clone!
;to make it work we use two turtles, one is the player
;and the other creates, moves and erases the pipes
;these are two seperate 'workers' or threads
;this procedure kicks off the game, displays a title
;screen, and then manages the player, processing
;input and moving the player appropriately
;the pipemaker turtle and its pipes procedure
;manages the pipes and checks for collisions
;the turtle does not move horizontally,
;instead the pipes do!
reset
cleartext
setbackgroundcolor pick [6 7 14 5 10 11 14 15]
;select a random background color from the given list
noaudiowait
;don't wait for audio to finish playing
setmodel [penup back 7 stamp "myrtle]
;we need to offset Myrtle's actual position more
;toward her center for the purposes of this game
setmodelscale 5
;make myrtle big!
playsound "doodoo
;startup sound
pipemaker:newworker [pipes]
;'kicks off' the pipemaker turtle, who
;creates and moves the pipes
;create the title screen:
penup
randomfillcolor
slideleft 180 forward 5
settypesize 60
pushturtle
;'push' the turtle's state (position etc) on
;to a stack, from which it can be 'popped'
;off later
typeset "Floaty
popturtle
;restore the previously pushed state
back 100
randfc
;short for randomfillcolor
pushturtle
typeset "Turtle
popturtle
bk 10 sr 70
;back and slideright
settypesize 10
randfc
typeset |Press any key to float!|
;title screen complete!
home
dn 90 rt 90
;down and right
make "raise 0
;the :raise container holds the current
;value remaining to float Myrtle upwards
forever [
if loopcount = 1 [say "Ready!]
if loopcount = 16 [say "Set!]
if loopcount = 31 [say "Go!]
;the loopcount is the number of times the forever
;loop has executed. Based on that count, say the
;appropriate ready set go component
if divisorp 100 loopcount [
;every 100 loops pick a random background
setbg pick [6 7 14 5 10 11 14 15]
;from the given list
]
;setbg short for setbackground
dosleep [50] [
;try to maintain an average of 50ms to do
;the following:
if loopcount > 30 [
;if the loopcount is greater than 30:
if :raise = 0 [
;if the value inside the :raise container
;is 0, then lower the turtle 2.5 turtle-units:
lower 2.5
]
[
;otherwise raise the turtle 2.5 turtle-units
;and decrease the value inside the :raise
;container by 2.5
raise 2.5
make "raise :raise - 2.5
]
if keyp [
;if a key is pressed, play a sound,
;remove the key from the keyboard buffer
;and increase the value of the :raise
;container by 20:
playsound "air
clearchar
make "raise :raise + 20
]
clean
;remove the turtle's 'track' -- it's not
;drawing or creating anything so we don't
;need to have it piling up
]
]
]
END
NEWTURTLE "pipemaker
TO pipes
;the pipemaker turtle's job is to
;create the pipes, shift them to the left
;and check if they've hit the turtle
clearscreen
noaudiowait
penup
home
slideright 250
back 120
up 90
;position the turtle appropriately for
;creating the pipes offscreen to the right
begintag "move
endtag
;this tag is used to shift the pipes,
;by replacing its contents with an ever-
;increasing slideleft directive
make "height 100
;this is the starting height of the pipes
make "heights [0 0 0 0 0]
;initialize an 'zeroes' list of pipe heights
make "score 0
make "count 0
;initialize the score and the pipe count
settypesize 20
;set the type size. Type is the text you
;create inside the 3D world
forever [
;do this forever:
sleep [50]
;try to do this stuff in an average of
;50ms -- less or more as needed to keep
;that average:
if divisorp 20 loopcount [
;every 20 loops:
inc "count
;increase the contents of the :count container
;by one
if loopcount > 60 [
;if the loopcount is greater than 60:
playsound "clang
inc "score
say :score
;make a sound, increment :score and say it
]
slideright 100
begintag loopcount
;create a new 'tag' for the pipe
;so we can remove it after it passes the
;left side of the screen
randomfillshade
;select a random fill shade
make "col 1 + random 5
;select a random number and put it in :col
setfillcolor item :col [12 9 7 6 11 14 10 13]
;set that color based on :col's index in the
;provided list
make "height :height + (-60 + random 120)
;increase or decrease :height based on a
;random number
if :height < 20 [make "height 30 + random 20] if :height > 120 [make "height 110 - random 20]
;if too high or low, pick a new value higher
;or lower as needed
queue :height "heights
;add the new height to the list of heights
;create the lower pipe:
cylinder 20 :height 50
lower :height
setfillcolor item :col [4 8 6 2 1 12 5 3]
;select the complementary color for the pipe
cylinder 25 20 50
lower 50
;type the pipe number:
down 90 right 90 back 10
if :count > 9 [bk 10]
inscribe :count
if :count > 9 [fd 10]
forward 10 left 90 up 90
;if gameplay is too slow, comment out
;the above five lines
;create the upper pipe:
lower 50
;remember, the turtle is upside down!
cylinder 25 20 50
lower 20
setfillcolor item :col [12 9 7 6 11 14 10 13]
cylinder 20 120 - ypos 50
raise :height + 120
endtag
;close the pipe 'tag'
if loopcount > 119 [erasetag loopcount - 100]
;erase the pipe that has left the screen
ignore pop "heights
;remove its height from the :heights container
;and just throw it away (ignore it)
]
if loopcount > 60 [
;check for crash:
if (or
myrtle:ypos > (-120 + (90 + item 3 :heights))
myrtle:ypos < (-120 + (20 + item 3 :heights))
;the third item in the :heights list is the
;height of the pipe around where the turtle
;is, so we can use that to see if we've hit
;anything
) [
(print |Crash! Final Score: | :score)
print |Press flag icon to play again!|
calm "myrtle
myrtle:run [setfillcolor 1 icosphere 30]
;red ball
audiowait
playsound "crack
playsound "aw
myrtle:clean
;gets rid of the red ball
noaudiowait
playsound "fall
say |too bad|
myrtle:repeat 90 [
forward 3 lower 3 wait 1
]
;fall to the ground
audiowait
playsound "crash
finish
;that's all folks
]
]
replacetag "move [slideleft loopcount * 5]
;increase the value of the slideleft
;command in the move tag, shifting the
;pipes to the left
]
;and that's it! Not really much code, is it?
END
If you hover over the various primitives in the editor, a popup will tell you what they do.
The game consists of three main parts: the dartboard, the darts and the game itself.
The dartboard is constructed primarily using the ringarc primitive and a number of repeat loops. It uses the oddp boolean to decide which color each segment of each ring should be, allowing us to match the colors of a standard dartboard.
The numbers around the outside of the dartboard are created using the inscribe and orbitleft primitives. We offset the numbers as required to center them on their relevant wedges. We also use the orbit / pullin / pullout functionality to create the wire frame overlaid on the dartboard.
dropanchor pullout 65 ra 2
foreach "i [20 1 18 4 13 6 10 15 2 17 3 19 7 16 8 11 14 9 12 5] [
lt 90 if :i > 9 [bk 10] [bk 5] inscribe :i
if :i > 9 [fd 10] [fd 5] rt 90 orbitleft 18
]
;numbers
home setpw 1.2 setpc 5 setfc 5
dropanchor tether pullout 5 orbitleft 9
repeat 6 [
pd
repeat 20 [orbitright 18 ico 0.6]
pu switch {repcount}
case 1 [pullout 5] case 2 [pullout 25]
case 3 [pullout 5] case 4 [pullout 20]
case 5 [pullout 5]
]
;metal rings
home pullout 10 orbitleft 9
repeat 20 [pd pullout 55 pu pullin 55 orbitright 18]
;metal lines
home
END
The darts are assembled using cylinders, cones, cutcones and poilyspots (for the fletchings).
TO dart :color
;create the dart models
lower 50 setfillcolor 10 cone 1 4 20
raise 20 cylinder 1 20 20
ra 10 setfc 5 cutcone 3.5 2 10 20
ra 20 setfc 10 cylinder 3 20 20
ra 5 setfc 5 cylinder 3.5 5 20
setfc item :color [11 6]
ra 20 cutcone 2.5 3 20 20
ra 5 setfc 10 cutcone 2 2.5 5 20
setfc 5 ra 5 cutcone 1.5 2 5 20
ra 10 setfc 0 cutcone 1 1.5 10 20
ra 20 cutcone 1 1 20 20 up 180
cone 1 15 20 rr 90
repeat 3 [
up 60 setfc 10 twosided
polyspot 13 6 setfc :color
ring 2 13 6
]
;fletchings
END
The game itself consists of a setup section and the main game loop. Inside the game loop we have the aiming section and the scoring section. Every three darts we switch between players. If one of the players score exceeds 301 then they are declared the winner and the game ends.
TO game
;world's smallest dart game
reset
clearfrozen
cleartext
print |Welcome to darts! Two players take turns until one scores more than 301.|
cursordown
print |Try to aim the dart with the mouse and press the mouse button to throw...|
The setup section creates the dart ‘room’, draws and ‘freezes’ the board (breaks it off into its own ‘turtle track’) and creates the dart models using the dart procedure. Default containers (variables) are created using surnames (container classes), one for each player. Surnames enable us to use the same code for each player without needing to use lists or tables.
pu sl 300 bk 300 ra 600
setfc pick [3 5 8 9] setfs 10
setbs fillshade setbg fillcolor
voxel -607 setfs 0 home
;create dart room
board
;create board
cam:pullout 180
freeze "board
newmodel "dart1 [dart 1]
newmodel "dart2 [dart 2]
;create dart models
setmodel "dart1
setmodelscale 0.7
setanchor [0 0 0]
foreach "i [red blue] [setsurname :i
rt 180 make "down forwarddir rt 180
make "updown random 2 make "leftright random 2
make "dart 0 make "frame 0 make "total 0
make "oldx mousex make "oldy mousey
]
setsurname "red
;define player containers
print word surname |'s turn...|
showturtle
setposition [0 0 250]
The main game loop moves the dart according to the mouse position. The dart moves up and down and side to side to simulate shaky hands, in an exaggerated fashion to make the game more challenging. The player pushes the left mouse button to launch the dart. The camera follows the dart in a method dictated by a random number.
forever [
;main game loop
if or :oldx != mousex :oldy != mousey [
make "oldx mousex make "oldy mousey
setposition {-100 + 200 * mousex / 100 50 - 100 * mousey / 100 250}
cam:sety myrtle:ypos
;move the dart with the mouse
]
move 1 random 360
if :updown = 0 [up 1 if and pitch < 350 pitch > 20 [make "updown 1]]
if :updown = 1 [dn 1 if and pitch < 350 pitch > 20 [make "updown 0]]
if :leftright = 0 [rl 0.5 if and roll < 350 roll > 10 [make "leftright 1]]
if :leftright = 1 [rr 0.5 if and roll < 350 roll > 10 [make "leftright 0]]
;shaky hands, maybe try drinking herbal tea?
wait 1
;delay
if buttonp 0 [
;if button clicked:
make "camdir random 4
;pick random camera action
dountil (item 3 extrapolate position vectorsub updir [0 0 0] 35) < 0 [
;until the tip of the dart hits the board (basically):
setpremodel {"lt loopcount}
;spin the dart model
lo 1 dn 0.1 cam:pullin 1
;toward the board and down a little
if :camdir != 3 [cam:sety myrtle:ypos]
if :camdir = 1 [cam:orbitleft 0.3]
if :camdir = 2 [cam:orbitright 0.3]
if :camdir = 3 [cam:orbitup 0.3]
;camera actions
]
Once the dart reaches the wall / board, we calculate if the dart hit the board or the wall by checking its distance from the center of the dart board [0 0 0]. If it actually hit the board then we 'stamp' the model in place.
norender
;stop rendering graphics while we deal with things
ht lo 34
make "vec vectors
setvectors originvectors
updatestate
;need to update the state for towards to work
;with rendering disabled
make "dir towards [0 0]
;where did we land relative to the center?
if and :dir >= 171 :dir < 189 [make "score 20]
if and :dir >= 189 :dir < 207 [make "score 1]
if and :dir >= 207 :dir < 225 [make "score 18]
if and :dir >= 225 :dir < 243 [make "score 4]
if and :dir >= 243 :dir < 261 [make "score 13]
if and :dir >= 261 :dir < 279 [make "score 6]
if and :dir >= 279 :dir < 297 [make "score 10]
if and :dir >= 297 :dir < 315 [make "score 15]
if and :dir >= 315 :dir < 333 [make "score 2]
if and :dir >= 333 :dir < 351 [make "score 17]
if or :dir >= 351 :dir < 9 [make "score 3]
if and :dir >= 9 :dir < 27 [make "score 19]
if and :dir >= 27 :dir < 45 [make "score 7]
if and :dir >= 45 :dir < 63 [make "score 16]
if and :dir >= 63 :dir < 81 [make "score 8]
if and :dir >= 81 :dir < 99 [make "score 11]
if and :dir >= 99 :dir < 117 [make "score 14]
if and :dir >= 117 :dir < 135 [make "score 9]
if and :dir >= 135 :dir < 153 [make "score 12]
if and :dir >= 153 :dir < 171 [make "score 5]
;calculate dart position and assign score
make "dist distance extrapolate position vectorsub updir [0 0 0] zpos [0 0 0]
;how far away from the center is the tip of the dart?
if :dist <= 5 [make "score 50]
;bullseye!
if and :dist > 5 :dist < 10 [make "score 25]
;half bullseye
if and :dist >= 35 :dist <= 40 [make "score :score * 3]
;triple ring
if and :dist >= 60 :dist <= 65 [make "score :score * 2]
;double ring
if :dist > 65 [make "score 0]
;missed!
We use the towards primitive (an original Apple Logo II primitive!) to determine the number of degrees the landed dart (which is in reality pointed upwards toward the ceiling, the dart 'descending' towards the board along the Z axis) would have to turn to face [0 0]. This, combined with the distance from the center allows us to calculate the dart's score.
(print |Dart| word :dart + 1 |:| :score) make "frame :frame + :score
setvectors :vec
if :dist < 79 [
;'stick' a dart to the board using stamp
playsound "click2
ra 34
run premodel
if surname = "red [stamp "dart1] [stamp "dart2]
render wait 120
]
else [
playsound "knock
pr "Missed! ra 34 st render
repeat (ypos + 300) / 4 [drift 4 :down cam:lo 4 wait 1]
]
;drop the dart to the floor
cam:cs
;reset the camera
setheading 0 setpitch 0 setroll 0
setvectors originvectors
;reset state
make "updown random 2
make "leftright random 2
;pick random starting wobble directions
;for the next dart
inc "dart
;add one to :dart
if :dart = 3 [
;if we've thrown three darts:
(print |Frame Score: | :frame)
make "total :total + :frame
(print word surname |'s Total Score: | :total)
make "dart 0 make "frame 0
cam:pushturtle
cam:run pick [[repeat 30 [orbitleft 1 wait 1]] [repeat 30 [orbitright 1 wait 1]]]
wait 120 cs cam:popturtle
if :total > 301 [(print surname "wins!) finish]
;the end
if surname = "red [setsurname "blue setmodel "dart2]
else [setsurname "red setmodel "dart1]
print word surname |'s turn...|
;switch players
]
setpremodel [] cam:pullout 180 setposition [0 0 250] showturtle
;position camera, reset 'spin', show the next dart
]
;end of throw
]
;end of main game loop
END
Logo code is like poetry! It's easy to read and describes what the computer is doing in fairly broad terms. This is why it has always been great as a first text-based coding language.