Cat Exercises

After the halfway point or so, check the Self Review notes to see if you're making any of the common mistakes.

Form pairs.

Download the basic virtual cat code to your Scheme project directory.

Open and run the code. The window should appear with the cat on the left.

Review quickly the way the cat structure is defined, and the cat API (application program interface), e.g., cat-x and so on.


Time for the big-bang

PLT Language Level: Beginner

animate is simple to use but you need to replace it with big-bang if you're going to do interactions.

Replace the (animate make-cat-scene) call with the equivalent big-bang form:

(big-bang 0
  (on-tick add1)
  (on-draw make-cat-scene))

Visible change when run: None


Replace time with cats

Look up big-bang for 2htdp/universe in the PLT Scheme Help Desk or here.

Change the "world" from an integer that starts with 0 and is updated with add1 to a cat that starts with cat-1 and is updated with update-cat

Define (update-cat cat) to return cat.

Change make-cat-scene to take a cat parameter instead of an integer and replace all occurrences of cat-1 with the parameter.

Visible change when run: None

Sanity check: Type in the Interaction Window

(make-cat-scene (new-cat 300 75 0))

A cat scene should appear with the cat over on the right.


Cat on the run

Change (update-cat cat) to return a new cat with the same Y and happiness but an X coordinate one more than that in cat.

Visible change when run: Cat slides to the right.


Multi-image cat

Change the cat structure definition to have two images: image1 and image2.

Copy the 2nd cat image from here.

Paste that 2nd image right after the previous cat image in the definition of new-cat. Make sure everything still works.

Visible change when run: None

Sanity check: Add this test to cat.ss:

(check-expect (equal? (cat-image1 cat-1) (cat-image2 cat-1)) false)

When you run your code, after you stop the animation, you should see a message saying that all tests passed.


Cat shuffle

Define (pick-image cat) to return the first or second cat image, depending on whether the X position of cat is odd or not. Call pick-image in make-cat-scene to choose the image to draw.

Run.

Visible change when run: The cat should shuffle to the right.


Cat direction

Now we want the cat to go back and forth. To do that we need to store a direction of travel inside the cat object. This will require changing our structure and functions.

Visible change when run: None

Sanity check: Add these tests to cat.ss:

(check-expect (cat-dx cat-1) 1)
(check-expect (cat-x (update-cat cat-1)) 51)
(check-expect (cat-dx (new-cat 10 20 -3 0)) -3)

When you run your code, after you stop the animation, you should see a message saying that all tests passed.


Cat control

Add the ability to change the cat's direction by pressing the "c" key.

To do this, add the form (on-key handle-key) to your big-bang call. This can go anywhere in the list of clauses, as long as it's after cat-1.

Adding this form tells big-bang to call handle-key whenever a key is pressed. big-bang will pass two arguments to handle-key:

handle-key, like update-cat, needs to return a cat object, either the same one, or a new updated one. See the big-bang documentation for more details.

Define (handle-key cat key) so that:

This means that when you press "c" the first time, the new cat's dx is -1 and the cat will move left. When you press "c" again, a new cat will be made whose dx is 1 and that cat goes right.

Use the function key=? not string=? to test keys.

Visible change when run: The cat should change directions every time you press the "c" key. What happens if you press capital "C"?


Add a happiness bar

Draw a red bar showing your cat's happiness level.

To draw a red rectangle, look up nw:rectangle in the Help Desk. nw:rectangle creates a rectangle of a given width, height and color. Define the following the global constants first:

Add a new place-image call to make-cat-scene to add a red rectangle with the above values. Use the cat's happiness for the width.

Visible change when run: You should see a shuffling cat and a short red bar.


Cats hate being told what to do

Change handle-key so that hitting the "c" key not only changes direction, but also decreases the cat's happiness by 5 points.

Visible change when run: Every time you hit the "c" key, the cat should change direction and the happiness bar should get shorter.


Cats like being petted

Change handle-key so that hitting the "p" increases the cat's happiness by 5 points. To keep the bar from growing too big,

Visible change when run: Every time you hit the "p" key, the happiness bar should get longer. The happiness bar should grow about as wide as the window and no further. Hitting the "c" key should still do what it did before.


The Red, Orange, Green show

Define (happy-bar-color width) to return

Use happy-bar-color to select the color to use to draw the happiness bar.

Visible change when run: The color of happiness bar should match the rules above.


Game over, man

Add (stop-when end-game?) to your big-bang call. This causes big-bang to call end-game? on every cycle to see if it's time to stop the animation. big-bang passes one argument, the "world" i.e., our cat, to end-game?.

See the big-bang documentation for more details.

Define (end-game? cat) to return true in three situations:

To test for being off screen, define (on-screen? posn) to be true if posn's X coordinate is ≥ 0 and ≤ WIN-WIDTH. Then test your cat's position with on-screen? in end-game?.

Note the use of "?" in the names end-game? and on-screen?. By convention, predicates in Scheme, i.e., functions that test for something being true or false, are given names ending with "?." Other languages have similar conventions.

Add these tests to cat.ss to make sure your on-screen? is working correctly:

(check-expect (on-screen? (make-posn 10 20)) true)
(check-expect (on-screen? (make-posn 0 20)) true)
(check-expect (on-screen? (make-posn -1 20)) false)
(check-expect (on-screen? (make-posn WIN-WIDTH 20)) true)
(check-expect (on-screen? (make-posn (add1 WIN-WIDTH) 20)) false)

Visible change when run: If you let the cat move off the screen in either direction, the game stops. If you hit the "c" key enough times to make happiness zero, the animation should stop. If you hit the "p" key enough times to make the bar the maximum length, the animation should stop.


The old grey cat

Right now it's too easy to make the cat ecstatic. But what if things speed up as the game progresses? The way to do this is to make the cat move faster as it gets "older." So, in this exercise, add age to your cat. This requires several changes:

Visible change when run: None

Sanity check: Add these tests to cat.ss:

(check-expect (cat-age cat-1) 0)
(check-expect (cat-age (update-cat cat-1)) 1)
(check-expect (cat-age (update-cat (update-cat cat-1))) 2)

When you run your code, after you stop the animation, you should see a message saying that all tests passed.


Old don't mean slow

Make the cat move faster as it gets "older." Define (cat-speed age dx) to return how fast a cat of a given age moves. Here's the formula:

dx + sign(dx) * 2 * ( age / 50 )

sign means "the sign of", i.e., -1 for negative numbers and +1 for positive. In PLT Scheme, the function to do this is sgn. For division, use quotient to do "integer division," e.g., 40 / 50 is 0 and 50 / 50 is 1.

This formula causes the cat to speed up by 2 pixels every 50 time steps.

Add these tests to cat.ss to make sure your cat-speed is working correctly:

(check-expect (cat-speed 2 1) 1)
(check-expect (cat-speed 2 -1) -1)
(check-expect (cat-speed 50 1) 3)
(check-expect (cat-speed 50 -1) -3)
(check-expect (cat-speed 99 1) 3)
(check-expect (cat-speed 99 -1) -3)
(check-expect (cat-speed 104 1) 5)
(check-expect (cat-speed 104 -1) -5)

Make sure you pass these tests first. Then change update-cat to use cat-speed to move the cat.

Visible change when run: The game should be much harder now. You have to pet quickly and change directions more frequently as the game goes on.


Herding cats

It's easy to herd one cat, but what about more? To do that, the "world" needs to change from a single cat to multiple cats. The simplest way to do that is to make the world object a list of cat objects. The following exercises gradually change the program to handle a list of cats.


Make room! Make room!

Make room or another cat by doubling the window height, WIN-HEIGHT. Run the code and make sure everything is still placed correctly.

Now modify make-cat-scene to draw the happiness bar just below the cat, with BAR-HEIGHT vertical space in between. Don't use hardwired numbers. Instead, define a function (cat-bar-y cat) that returns the correct Y to use to place the bar below the cat, using cat-y and BAR-HEIGHT. Call that function in make-cat-scene.

Visible change when run: The bar is up near the cat but not overlapping, with plenty of whitespace below the bar.


List-less no more

Change your program so that the "world" is a list of just one cat. It should work the same as before, but will be closer to ready for more cats.

Change the big-bang form as follows:

Now define each of these four new functions.

Visible change when run: None


There's always another cat

Time to define a function to create another cat, positioned below the existing cat.

Define (next-cat cat) to return a new cat, whose X coordinate is the same as cat and whose Y coordinate is such that the new cat is below the happiness bar of cat, with BAR-HEIGHT space in between. next-cat should calculate the Y coordinate from information about cat.

Change the list in the big-bang call to be (list cat-1 (next-cat cat-1)).

Since the world now contains two cats, you have to fix the list calls in handle-key-cats and update-cats to return the second cat as well as the result for the first cat. You shouldn't call any function on the second cat, for now.

Visible change when run: None. You haven't changed your code to draw both cats yet.


Peek-a-boo, I see you!

two cats example Now to fix make-cats-scene to show all cats in the list. This is going to require a recursive loop over the list of cats. Use the basic list recursion pattern, where

You will no longer be using your make-scene function. Don't try to shoehorn it into make-cats-scene.

Visible change when run: Both cats should appear. Only the top cat moves. Make sure nothing overlaps.


Two cats, no waiting

Now update both cats.

You could do this by writing (list update-first-cat update-second-cat) but this only works if you have exactly two cats, no more, no less. The better way is to write a loop that updates each cat in the list, no matter how many cats you have.

Change update-cats to use the basic list recursion pattern for building a list of updated cats, where

Visible change when run: Both cats move across the screen. Only the first cat responds to the keyboard commands.


I'm pointing at you, cat!

Right now, any key press only affects the top cat. Add the ability to control either cat by letting the user click on a cat. Whichever cat was last clicked on becomes the one that responds to key presses.

cat bounding box The first thing you need to do is to figure out which cat was clicked on, if any, when the user clicks the mouse. The way this work in interactive graphics programs is that when the user clicks the mouse, the program is given the X and Y coordinates of where the cursor was. It's up to your program to figure out if the point (X, Y) is where a cat is or not.

Define (cat-contains-xy? cat x y) to return true if the point (X, Y) is in the "bounding box" for cat. As shown in the figure, the bounding box is the smallest rectangle that surrounds the image. The bounding box can be calculated from the image's width and height and position. Don't worry that some of the area of the rectangle is empty. Users will be happy that you let them be a little sloppy in clicking.

You're almost certain to make mistakes defining cat-contains-xy?, so it's really critical to do test-driven development here. Add the following tests to your code. Make sure they pass first.

(check-expect (cat-contains-xy? cat-1 0 0) false)
(check-expect (cat-contains-xy? cat-1 50 75) true)
(check-expect (cat-contains-xy? cat-1 87 75) true)
(check-expect (cat-contains-xy? cat-1 14 75) true)
(check-expect (cat-contains-xy? cat-1 50 133) true)
(check-expect (cat-contains-xy? cat-1 50 17) true)
(check-expect (cat-contains-xy? cat-1 87 133) true)
(check-expect (cat-contains-xy? cat-1 14 17) true)
(check-expect (cat-contains-xy? cat-1 100 75) false)
(check-expect (cat-contains-xy? (next-cat cat-1) 50 75) false)

This and the next few exercises do not make any changes to the graphic interaction. They just have functions to create and tests to pass. To make it easier to see the results of tests, use the Scheme | Comment Out with Semicolons menu command to temporarily comment out the entire big-bang form. That way, when you run, just the tests will execute.


I found you!

Now that you can tell if a cat contains an (X, Y) point, the next step is to write a function that can find a cat that contains (X, Y) in a list of cats.

Define (find-clicked-cat x y cats) to recursively loop over a list of cats and return the first one that contains the point (x, y). If so such cat is found, it should return false.

Here are a test cases that your code should pass:

(check-expect (find-clicked-cat 50 75 (list cat-1 (next-cat cat-1))) cat-1)
(check-expect (find-clicked-cat 50 75 (list (next-cat cat-1) cat-1)) cat-1)
(check-expect (find-clicked-cat 87 133 (list cat-1 (next-cat cat-1))) cat-1)
(check-expect (find-clicked-cat 0 0 (list cat-1 (next-cat cat-1))) false)

Your turn

Now that you can find which cat was clicked on, the easiest way to make it the cat that responds to mouse clicks is to put it first in the list of cats, because the first cat is what your handle-key-cats looks at.

You just defined a function that returns the cat clicked on, if any, or false. So now, define (promote x l) to take an object of any kind and a list of objects. If x is in the list and not the first element, it returns a new list with x first and the other elements following. If x is false, or the first element, it just returns the original list.

Here are some test cases. Notice that they're not lists of cats. This function is a very general function that should work with a list of anything.

(check-expect (promote 2 (list 1 2)) (list 2 1))
(check-expect (promote 3 (list 1 2 3)) (list 3 1 2))
(check-expect (promote 1 (list 1 2 3)) (list 1 2 3))
(check-expect (promote false (list 1 2 3)) (list 1 2 3))

Here, mousie, mousie

Time to handle mouse clicks.

If you commented out big-bang in the previous exercises, remove the semicolons now, using Scheme | Uncomment.

By now, you can probably guess how you tell big-bang to call a function when the mouse is clicked. Add the form

(on-mouse handle-mouse-cats)

to your big-bang form. To start, define (handle-mouse-cats cats x y event) to do nothing but return cats.

Run and make sure that no error -- or anything else for that matter -- happens when you start or when you click the mouse. Don't continue until this has been verified.

Now add code to see if the user clicked on a cat. When the user clicks the mouse button, the system calls handle-mouse-cats with the "world" -- i.e., your list of cats -- plus the X and Y coordinates of where the on-screen cursor was at the time the button was released, plus the "button-up" event object. (There are many other mouse events, described in the on-mouse documentation but "button-up" is the only one you should respond to.

Define handle-mouse-cats to test if the event is "button-up" using mouse=? to compare the event with the string "button-up".

If the event is not "button-up" then just return the list of cats unchanged. If the event is "button-up", then use the functions you've just defined to find the clicked cat, if any, and promote it to first place in the list of cats.

Visible change when run: Clicking a cat should make it the one that responds to future key presses.


If one cat ain't happy, ain't no one happy

Right now you should be able to win the game by holding down the "pet" key until the first cat is ecstatic. But life with cats isn't like that. Try that and make sure that happens.

Change end-cats-game? to return true when

Notice that it only takes one cat running away or getting furious to lose the game, but all cats must be happy to win the game. So you'll have to think a bit about how to do this logic. You'll need a recursive loop -- maybe two!

Visible change when run: Holding down the "pet" key until the first cat is ecstatic no longer stops the game.


Just to rub it in

end game scene Although both you and the computer know whether you won or lost, nothing shows that at the end of the game.

Fortunately (except for your pride) PLT Scheme makes it pretty easy to draw a final picture showing the final status of the game.

Change (stop-when end-cats-game?) to (stop-when end-cats-game? end-cats-scene). This is a variant form of the stop-when clause for big-bang designed just for this kind of thing. The system will display whatever scene end-cats-scene constructs when the game stops.

Define (end-cats-scene cats) to draw the normal cats scene, but to add to it a large (32 or 36 point font) piece of indigo text that says "You won!" or "You lose" depending on the state of the cats, i.e., you win if all cats are ecstatic, you lose otherwise.

Use the function text to create the text image.

As Columbo would say, "just one more thing..."

Center the text in the middle of the game window. Do this by defining a function (center-image image scene) that places image in the center of scene. Do this using the widths and heights of both arguments, rather than hardwired numbers or global variables. Scenes are a kind of image so you can use image-width and image-height with them. That's better than using the global constants WIN-WIDTH and WIN-HEIGHT.


Cats like it clean

Now that you know about map and similar higher-order functions, you should be able to replace all your explicit recursive loop functions with calls to map, ormap, etc. Change one function at a time and make sure everything still works.

Then clean up the rest of your code. Look for repetitions of code, longer than half a line or so. Refactor the common code into a well-named function, so that there's no repetition.

If you get stuck or aren't sure about either of these steps, ask for help.


The More, the Merrier

Here's a good test of how general you've made your code. Ideally, you should be able to increase the number of cats simply by adding more next-cat calls in the big-bang expression. Nothing else in your code should depend on the specific number of cats.

To do this, you need to stop using a fixed window height, i.e., WIN-HEIGHT. Define a function (calc-win-height cats) to calculate the appropriate window height for the given list of cats. This code can use constants like BAR-HEIGHT but should not assume a specific cat image height, even that all cats have the same height. Try to do this with a higher-order function, not an explicit loop.

Use calc-win-height in your make-cats-scene function.

Test. The window might be slightly different in height than it was with WIN-HEIGHT, but should look basically the same.

Now define (make-cats cat-1 n) to make a list of N cats based on cat-1, e.g., (make-cat 4) should make a list of 4 cats, the same as would be produced if you did (list cat-1 (next cat-1) (next-cat (next-cat cat-1)) (next-cat (next-cat (next-cat cat-1)))).

Then change the list of initial cats in big-bang from (list cat-1 (next-cat cat-1)) to (make-cats cat-1 2). There should be no change.

Now try (make-cats cat-1 4). There should be 4 cats in the game, all moving and responding properly.


nfold me in your arms

The function make-cats can be generalized into a cross between foldr and build-list.

Define (nfold-list fn x n) to take a function of one argument, an initial value for fn, and a non-negative integer n. It should return a list of n elements, containing (list x (fn x) (fn (fn x)) (fn (fn (fn x))) ...).

Redefine make-cats to call nfold-list.

Test. Everything should still work.


To do the following exercises, you need to be in Advanced Student mode. This is also a good time to start a new file of code with a new name, e.g., cat-v2.ss, just in case you break something badly.


Copy cat, copy cat!

This is a simple function but necessary for the next exercise. First add the following test cases

(check-expect (equal? cat-1 (copy-cat cat-1)) true)
(check-expect (eq? cat-1 (copy-cat cat-1)) false)
(check-expect (equal? (cat-posn cat-1) (cat-posn (copy-cat cat-1))) true)
(check-expect (eq? (cat-posn cat-1) (cat-posn (copy-cat cat-1))) false)

In Scheme, eq? is true if two objects are the actual same object. equal? is true if two objects have the same value, which, for structures, means their parts are equal?. So the above tests check to see if copy-cat returns a new but equal cat. Then they check to see if the position structure inside the copy is also new but equal.

Make sure these tests fail. Then define (copy-cat cat) to construct a copy of cat with no shared data structures.


Inscrutable but mutable

Your update-cat function is called 26 times a second. Each calle constructs a new cat object for each cat in the list of cats. The old cat objects are simply forgotten by the code. To get back the memory used for them, Scheme has to periodically run a process called garbage collection. Garbage collection is a relatively expensive process. In a long-running game you want to avoid generating too much garbage.

You're going to change (update-cat cat) to modify cat with structure mutation functions. update-cat will not create any new cat. It will just modify an existing cat.

First, you need to fix your test cases. You can no longer do things like this:

(check-expect (cat-age (update-cat cat-1)) 1)
(check-expect (cat-age (update-cat (update-cat cat-1))) 2)

If update-cat changes cat-1, then the second test will fail because cat-1 will have age 1, so the result will be 3. Worse, when your game starts, cat-1 will have age 3, not 0. You will have a "used" cat!

You want to always test with a copy of cat-1. So the above tests should look like this:

(check-expect (cat-age (update-cat (copy-cat cat-1))) 1)
(check-expect (cat-age (update-cat (update-cat (copy-cat cat-1)))) 2)

Similarly, change big-bang to start with a copy of cat-1. Run and make sure the game still runs as expected and these tests pass. If other tests with update-cat fail, fix those too.

Then add this test:

(check-expect (let ((cat (copy-cat cat-1))) (eq? (update-cat cat) cat)) true)

This test creates a copy, updates it, and checks to make sure that the cat returned by update-cat is exactly the same cat, not a new one.

Run this test and make sure it fails.

Then change update-cat to use the mutation functions for cat and posn to update the cat's age and X coordinate.

Then run your game. Make sure it still runs the same and all the tests pass.


Valid HTML 4.01 Strict