CS 5010: Guided Practice 3.1: The Falling Cat Stops

Here are two solutions, one ok and one better.

Solution 1

The first thing we need are tests for this new behavior. Following our general pattern, we create example worlds for the tests and give them mnemonic names. Then we add the test themselves.

    ;; example world with cat at bottom
    (define cat-at-bottom
      (make-world 
       (- CANVAS-HEIGHT (/ (image-height CAT-IMAGE) 2)) 
       false))
    
    ;; example world with cat 0.5*CATSPEED above the bottom
    (define cat-near-bottom  
      (make-world
        (- CANVAS-HEIGHT
          (/ (image-height CAT-IMAGE) 2)
          (/ CATSPEED 2))
        false))
    
    ;; Then we add tests to world-after-tick-tests :
    
    (begin-for-test
    
      ;; ...keep old tests...
    
      (check-equal?
        (world-after-tick cat-near-bottom)
        cat-at-bottom
        "cat near bottom should stop at bottom")
    
      (check-equal?
        (world-after-tick cat-at-bottom)
        cat-at-bottom
        "cat at bottom should stay there"))
  

Now we change world-after-tick-helper. We include the definition of world-after-tick here for completeness.

    ;; world-after-tick : World -> World
    ;; produce the world that should follow the given world after a tick
    
    ;; examples: 
    ;; cat falling:
    ;; (world-after-tick unpaused-world-at-20) = unpaused-world-at-28
    ;; cat paused:
    ;; (world-after-tick paused-world-at-20) = paused-world-at-20
    ;; cat near bottom:
    ;; (world-after-tick cat-near-bottom) = cat-at-bottom
    
    ;; strategy: structural decomposition [World]
    (define (world-after-tick w)
      (world-after-tick-helper (world-pos w) (world-paused? w)))
    
    ;; tests: tests follow help function.
    
    ;; world-after-tick-helper : Integer Boolean -> World
    ;; given a position and paused?, produce the next World
    ;; strategy: function composition
    (define (world-after-tick-helper pos paused?)
      (if paused? 
          (make-world pos paused?)
          (if (would-cat-go-past-bottom? pos)
            cat-at-bottom
            (make-world (+ pos CATSPEED) paused?))))
    
    ;; would-cat-go-past-bottom? : Integer -> Boolean
    ;; would a cat at the given coordinate go past the bottom
    ;; if it continued at CATSPEED?
    (define (would-cat-go-past-bottom? pos)
      (>
       (+ pos CATSPEED (/ (image-height CAT-IMAGE) 2))
       CANVAS-HEIGHT))
  

Note that we've encapsulated the complicated arithmetic into a help function, so we know what the arithmetic is intended to mean.

A better solution:

We observe that there is a fixed upper limit on the y-position of the cat, so we define it as a constant, and then in world-after-tick helper we use the function min to enforce this limit.

    (define CAT-Y-LIMIT (- CANVAS-HEIGHT (/ (image-height CAT-IMAGE) 2)))
    
    ;; world-after-tick-helper : Integer Boolean -> World
    ;; given a position and paused?, produce the next World
    ;; strategy: function composition
    (define (world-after-tick-helper pos paused?)
      (if paused? 
          (make-world pos paused?)
          (make-world 
            (min (+ pos CATSPEED) 
                 CAT-Y-LIMIT)
            paused?)))
  

Of course we use the new tests in this solution also.

This solution is cleaner for the falling cat, but the general idea of would-cat-go-past-bottom? is useful in many circumstances: balls bouncing off walls, etc. We call the would-cat-go-past-bottom? test a GUARD, because it guards against the cat going past the bottom. In the old days, we would have tests like end-of-file? which answered the question: if you tried to do a read now, would you go past the end of the file?

For debugging: Click here to validate.