Anything worth doing is worth over doing, right? This time we have another two problems from Programming Praxis, aptly title “The First Two Problems“. The whole thing is based on the idea from Brian Kernighan and Dennis Ritchie that the first two programs every programmer should be able to write are a simple Hello World problem and one that can print out a conversion table between degrees Fahrenheit and Celsius. So of course I decided to write it all using Turtle Graphics instead of just printing the characters.
As always if you would like to download the entire source code, you can do so here. Granted, you’ll almost surely need Wombat to run it as it depends on the
(c211 turtle) library, but the code should hopefully be easy enough to read through even without that.
Now that that’s out of the way, let’s start with a bit of a spoiler, here’s the output I’m going for. First, Hello World:
Then, the conversion table:
Turns out, it doesn’t look half bad. Particularly considering a bulk of the time was spent coming up with the points for a turtle graphics font. Speaking of which, here’s what such a font might look like in the case of drawing numbers:
; define positions for digits (define digits '#(((0 3) (3 6) (9 6) (12 3) (9 0) (3 0) (0 3)) ; 0 ((0 3) (12 3) (9 0)) ; 1 ((0 6) (0 0) (6 6) (9 6) (12 3) (9 0)) ; 2 ((3 0) (0 3) (3 6) (6 3) (9 6) (12 3) (9 0)) ; 3 ((6 6) (6 0) (12 6) (0 6)) ; 4 ((12 6) (12 0) (9 0) (6 6) (3 6) (0 3) (0 0)) ; 5 ((9 6) (12 3) (9 0) (3 0) (0 3) (3 6) (6 3) (3 0)) ; 6 ((12 0) (12 6) (0 3)) ; 7 ((3 0) (0 3) (3 6) (9 0) (12 3) (9 6) (3 0)) ; 8 ((3 0) (0 3) (3 6) (9 6) (12 3) (9 0) (6 3) (9 6)) ; 9 ((6 0) (6 6)))) ; -
Each of the characters is specified by a series of row/column coordinates with 0,0 in the bottom left with rows ranging from 0 to 12 and columns from 0 to 6. A line will be drawn starting at the first pair in the list and then through each of the rest in turn. I also added the special character
- for negative numbers. There’s another table for the letters, which you can see in the source code.
Now the big question is how do you actually draw those? It turns out, it’s not so bad. Here’s the first function which takes a vector of point lists (like
digits above) and draws the list at a given point with the given turtle:
; using turtle t, draw the digit/letter with index i from table chars ; with the top left at r, c ; reset the turtle to wherever it was after that (define (draw-thing chars t r c i) (block t (let ([ps (vector-ref chars i)]) (lift-pen! t) (move-to! t (+ c (cadar ps)) (+ r (caar ps))) (drop-pen! t) (let loop ([ps (cdr ps)]) (unless (null? ps) (move-to! t (+ c (cadar ps)) (+ r (caar ps))) (loop (cdr ps)))))))
Pretty straight forward. Save the state, lift the pen to move to the first point, and then recursively draw lines from point to point. Now what if we want to use that to draw a number? Still pretty straight forward. Just loop through the digits, drawing them one at a time. The hardest part was actually formatting them from the right so that the standard
div method for extracting digits would work.
; using turtle t, draw a number n with the top left at r, c ; reset the turtle to whever it was after that (define (draw-number t r c n) (block t (if (= n 0) (draw-thing digits t r (+ c 10) 0) (let ([? (< n 0)]) (let loop ([c (+ c (* 10 (digits-in n)) 1)] [n (abs n)]) (when (> n 0) (draw-thing digits t r c (mod n 10)) (loop (- c 10) (div n 10))) (when (and ? (= n 0)) (draw-thing digits t r c 10)))))))
In case you were wondering, here’s the function that will tell me how many characters are in a number, including an extra one for the
- in negative numbers:
; helper to calculate how wide a number is (add one for negative numbers) (define (digits-in n) (if (= n 0) 0 (+ (if (< n 0) 1 0) (inexact->exact (+ 1 (floor (log (abs n) 10)))))))
After getting numbers working, drawing strings was much easier. Particularly because you can directly access the letters from left to right with
; using turtle t, draw a string s with the top left at r, c ; reset the turtle to whever it was after that (define (draw-string t r c s) (block t (for-each (lambda (i) (unless (eq? #\space (string-ref s i)) (draw-thing letters t r (+ c (* i 10)) (- (char->integer (char-upcase (string-ref s i))) 65)))) (iota (string-length s)))))
And that’s it. Well, except for the minor fact that we haven’t actually solved the problem yet.
; draw a temperature table ; (draw-image (make-temperature-table)) (define (make-temperature-table) (let ([t (hatch)]) (draw-string t 15 0 " F") (draw-string t 15 50 " C") (for-each (lambda (row) (let* ([f (* row 20)] [c (inexact->exact (round (/ (* (- f 32) 5) 9)))]) (draw-number t (* (- row) 15) 0 f) (draw-number t (* (- row) 15) 50 c))) (iota 16)) (turtle->image t))) ; draw hello world ; (draw-image (hello-world)) (define (hello-world) (let ([t (hatch)]) (draw-string t 0 0 "Hello World") (turtle->image t)))
That was actually really fun to do. Perhaps I’ll see what other
trouble fun I can get into with it.
As a random side note, it’s amusing to watch the turtles actually going about the drawing. Try turning on
|← A Sea of Stars – Ch. 4 – Troubleshooting||All||Generating Voronoi diagrams →|
|← Generating epubs||By category||Generating Voronoi diagrams →|