; PROJECT 2, due Friday, May 9, 2008 ;-------------------------------- ; simulation of simple foxes-rabbits-grass world ; implementation of two-dimensional arrays (define (make-array rows cols fill) ; fill is the initial value of each element (do ((a (make-vector rows)) (n 0 (+ n 1))) ((= n rows) a) (vector-set! a n (make-vector cols fill)))) (define (array-ref array row col) (vector-ref (vector-ref array row) col)) (define (array-set! array row col obj) (vector-set! (vector-ref array row) col obj)) ; The world of the simulation consists of locations arranged in a ; world-size x world-size grid. (define world-size 20) (define grass-world (make-array world-size world-size #f)) (define animal-world (make-array world-size world-size #f)) ; Each location in the world will be represented by a o-size x o-size pixel ; square in the graphics window, so the window size (width, height) is ; o-size * world-size. (define o-size 20) ; Also the width and height of an object (animal or grass) (define size (* o-size world-size)) ; Each world location will be empty or will contain one object, either an ; animal or some grass. The basic simulation routine simply scans a ; random list of the objects in the world and sends a 'tick message to each ; object. The 'tick message will cause grass objects to grow and spread, ; rabbits to eat grass and give birth to other rabbits, and foxes to eat ; rabbits, fight each other and give birth to other foxes. ; In order to be fair to all the objects in the simulated world, the objects ; have to be given their 'tick messages in a different order in each cycle of ; the simulation. The function rearrange takes a list as its argument and ; returns another list containing the same objects as the input list, but in ; a random order. (Well, it won't be quite random, but it should be random ; enough for our purposes.) Using an auxiliary function, rearrange steps ; through the list and removes every third object from the list and puts it ; in a new list that it is building. The way to remove an object from ; the original list if the auxiliary function has cdred down the list to ; its current-position in the list, is to set a local variable to the cadr of ; the current-position, set the cdr of the current-position to the cddr of ; the current-position, and then call the auxiliary function with the ; current-position and the object saved in the local variable consed onto ; the new list. Since the first object in the original list will have to ; be removed at some time and put on the new list, but all objects are removed ; by setting the cdr of a pair to the cddr of the pair, rearrange conses a ; dummy object onto the front of the original list before passing it to the ; auxiliary function. Also, when the auxiliary function finds itself at the ; last pair in the list, it immediately calls itself with the entire list ; (beginning with the dummy object) again and continues cdring down the list ; until it finds the next third object to be removed. The auxiliary function ; will also need to keep track of the number of objects yet to be removed from ; the list; every time it removes an object and puts it on the new list, it ; decrements this number. When the number reaches 0, the original list ; contains only the dummy object. The new list, which now contains all of the ; original objects (but in a different order) is then returned. ; To illustrate, and to provide a test case, here is a trace of the behavior ; or rearrange: ; (define a '(0 1 2 3 4 5 6 7 8 9)) ; (set! a (rearrange a)) ; a ; (3 9 4 7 0 6 1 8 5 2) ; (set! a (rearrange a)) ; a ; (7 2 0 8 3 1 9 5 6 4) ; Hints: In rearrange, you can use a local variable to hold the original ; list with the dummy object consed onto it. Then define the nested auxiliary ; function with four arguments: the current-position it is at in the list, ; the number of objects still to be removed from the list, the count of the ; number of cdrs it has yet to do before removing an object, and the new list ; on which the auxiliary function puts the object once it is removed from ; the cadr of the current-position and the cdr of the current-position is set ; to the cddr of the current-position. The auxiliary function is called with ; the current-position being the entire list (including the added dummy ; object), the number of items to be removed being the length of the original ; list (before the dummy object was added), the cdr count being 2, and the new ; list being initially empty. ; YOUR ASSIGNMENT, PART 1: Implement the rearrange procedure. ; Turn in a listing of your code. (define (rearrange lst) ; You supply definition here. ; On each cycle of the simulation, update-world will combine the new-objects ; with the current active-objects. Then it will filter this list, leaving ; out the objects that are no longer alive. Next it rearranges this list ; and finally it sends a 'tick message to every object on the list. If the ; list contains no animals, update-world returns #f to indicate that the ; simulation is over. (define new-objects ()) (define active-objects ()) (define (filter proc list) (cond ((null? list) ()) ((proc (car list)) (cons (car list) (filter proc (cdr list)))) (else (filter proc (cdr list))))) (define (update-world) (set! active-objects (rearrange (filter (lambda (obj) (obj 'alive?)) (append new-objects active-objects)))) (set! new-objects ()) (if (zero? (rabbit-count active-objects)) #f (map (lambda (obj) (if (obj 'alive?) (obj 'tick))) active-objects))) (define (rabbit-count lst) (cond ((null? lst) 0) ((eq? ((car lst) 'type) 'rabbit) (+ 1 (rabbit-count (cdr lst)))) (else (rabbit-count (cdr lst))))) ; The simulation simply calls update-world over and over until ; the specified cycle limit has been reached or until there are ; no more rabbits. It returns 'done if it reaches the limit (n) ; or it returns the number of cycles it did before all the rabbits ; disappeared. (define (run n) (define (run-aux t) ;(sleep 0.5) ; uncomment to slow down simulation (display t) (newline) (if (= t n) 'done (if (update-world) (run-aux (+ t 1)) t))) (init-graphics-window) (init-world) (run-aux 0)) ; The objects in the world consist of grass and animals. ; The locations of the initial grass and animals are given ; by listing their i and j coordinates in separate lists: (define grass-list '((1 5) (4 3) (6 9) (2 6) (7 2))) (define rabbit-list '((7 5) (4 2) (2 9))) (define fox-list '((4 9) (2 2))) ; Init-world puts the animals and grass in the world using ; the above lists. (define (init-world) (set! new-objects ()) (set! active-objects ()) (do ((i 0 (+ i 1))) ((>= i world-size)) (do ((j 0 (+ j 1))) ((>= j world-size)) (array-set! grass-world i j #f) (array-set! animal-world i j #f))) (map plant-grass grass-list) (map place-rabbit rabbit-list) (map place-fox fox-list)) (define (plant-grass ij) (let ((i (car ij)) (j (cadr ij))) (let ((new-grass (make-grass i j (random max-growth)))) (array-set! grass-world i j new-grass) (set! new-objects (cons new-grass new-objects)) (new-grass 'draw)))) (define (place-rabbit ij) (let ((i (car ij)) (j (cadr ij))) (let ((new-animal (make-rabbit i j (+ (random rabbit-breed-level) 1)))) (array-set! animal-world i j new-animal) (set! new-objects (cons new-animal new-objects)) (new-animal 'draw)))) (define (place-fox ij) (let ((i (car ij)) (j (cadr ij))) (let ((new-animal (make-fox i j (+ (random fox-breed-level) 1)))) (array-set! animal-world i j new-animal) (set! new-objects (cons new-animal new-objects)) (new-animal 'draw)))) ; A rabbit will need to choose randomly from its adjacent locations. ; It will need to do this in order to move, and when it has enough energy ; to reproduce, it will need to do this to determine where its offspring ; will be born. The function that computes such a random spot is next-spot. ; If there are no adjacent locations, it returns the same spot it started from. (define (next-spot i j) (do ((candidates ()) (di -1 (+ di 1))) ((> di 1) (if (null? candidates) (list i j) ; no adjacent spots (let ((choice (random (length candidates)))) (list-ref candidates choice)))) (do ((dj -1 (+ dj 1))) ((> dj 1)) (let ((ii (+ i di)) (jj (+ j dj))) (if (and (>= ii 0) (< ii world-size) (>= jj 0) (< jj world-size)) (set! candidates (cons (list ii jj) candidates))))))) ; The remaining code depends on graphics procedures. (require (lib "graphics.ss" "graphics")) (open-graphics) (define w (open-viewport "simulation" size size)) (define (init-graphics-window) ((clear-viewport w))) (define green (make-rgb 0 1 0)) (define red (make-rgb 1 0 0)) (define white (make-rgb 1 1 1)) (define blue (make-rgb 0 0 1)) ; Animals and grass will be defined as procedures that ; respond to messages. We need a constructor procedure ; to create these procedures for us. ; Grass will be represented by a green rectangle of size ; o-size pixels by o-size pixels. If the world coordinates ; of the grass are i,j then the green rectangle will be ; drawn at location o-size * j, o-size * i in the graphics ; window. (define rectangle (draw-solid-rectangle w)) ; Note that with the rectangle procedure, a green square ; can be drawn at point x,y in the graphics window with the ; command (rectangle (make-posn x y) o-size o-size green). ; Grass has a location i,j in the world, and an internal growth ; parameter. Growth starts with value 0 and increments by 1 ; with each cycle of the simulation. When growth reaches the ; value of max-growth, the grass reproduces in every adjacent ; space, and the growth parameter is reset to 0. (define max-growth 3) ; The above paragraph describes what grass does in response to ; the message 'tick. The other messages that it responds to are: ; draw -- the grass draws itself at the appropriate place in the ; graphics window. ; refresh -- the grass draws itself but also any animal ; standing on it. ; type -- returns the value 'grass. ; alive? -- returns #t if it is still alive, #f if it has been eaten. ; chomp -- the grass has been eaten; grass is now dead. ; Here is code for making a grass procedure: (define (make-grass i j growth) (let ((living #t)) (lambda (msg) (cond ((eq? msg 'tick) (set! growth (+ growth 1)) (if (= growth max-growth) (begin (set! growth 0) (do ((di -1 (+ di 1))) ((> di 1)) (do ((dj -1 (+ dj 1))) ((> dj 1)) (let ((ii (+ i di)) (jj (+ j dj))) (if (and (>= ii 0) (< ii world-size) (>= jj 0) (< jj world-size) (not (array-ref grass-world ii jj)) ) (let ((new-grass (make-grass ii jj 0))) (array-set! grass-world ii jj new-grass) (new-grass 'refresh) (set! new-objects (cons new-grass new-objects)))))))))) ((eq? msg 'draw) (rectangle (make-posn (* o-size j) (* o-size i)) o-size o-size green)) ((eq? msg 'refresh) (let ((me (array-ref grass-world i j)) (animal (array-ref animal-world i j))) (me 'draw) (if animal (animal 'draw)))) ((eq? msg 'type) 'grass) ((eq? msg 'chomp) (array-set! grass-world i j #f) (set! living #f) (rectangle (make-posn (* o-size j) (* o-size i)) o-size o-size white)) ((eq? msg 'alive?) living) (else (error "bad message to grass" msg)))))) ; Rabbits will be implemented in a similar way to the implementation ; of grass. A rabbit will be displayed as a red circle made with ; the ellipse procedure: (define ellipse (draw-solid-ellipse w)) ; If a rabbit is located in the world at i,j, it is drawn in ; the graphics window by the command ; (ellipse (make-posn (* o-size j) (* o-size i)) (- o-size 2) (- o-size 2) red) ; You have to define the make-rabbit procedure that makes the ; procedure that accepts messages. When a rabbit procedure ; is created, it has a location i,j and an internal energy. ; This energy is initialized to the birth-energy level. (define birth-energy 20) ; The messages that a rabbit can receive are the following: ; tick -- The rabbit subtracts 1 from its energy. If the energy gets ; to be <= zero, the rabbit dies; it does the same thing that it would ; do if it received the 'chomp message. If the energy ; is still greater than 0, it gets hold of itself by calling ; (array-ref world i j) and moves itself to a ; new location obtained by calling 'next-spot. ; Note: the rabbit has to draw a white rectangle in the square where it ; was and a red circle where it has just moved to. (If the next spot is ; the same as the current spot, there is no need to draw anything.) ; If the location that the rabbit is about to move to is occupied by grass, ; the rabbit sends a 'chomp message to the grass to kill it and increments ; its own energy by the value of 'grass-food-value before moving to the new ; location. Note: if there is an animal standing on the grass, the rabbit ; doesn't do anything except lose energy. (define grass-food-value 10) ; If the energy is >= rabbit-breed-level, the rabbit gives birth ; to another rabbit in an adjacent location. Use the next-spot ; procedure to get the adjacent location. If that location is empty, ; create the rabbit with 'make-rabbit and put it in the adjacent location ; in the world array. Also subtract birth-energy from the (parent) ; animal's energy; this is the cost of giving birth. Don't forget to ; put the new rabbit on the new-objects list. (define rabbit-breed-level 40) ; chomp -- This is the message that a fox sends to a rabbit when it eats the ; rabbit. The rabbit must remove itself from the animal-world array, mark itself as ; not living, and draw a green or white rectangle where it used to be. ; draw -- The animal draws itself in the graphics window. ; type -- Returns the value 'rabbit. ; alive? -- returns #f if it is no longer alive. ; YOUR ASSIGNMENT, PART 2: Define the make-rabbit procedure so that it ; returns a procedure that responds to the above messages as described. ; Turn in a listing of your code. ; Foxes are similar to rabbits with the following differences; ; Foxes receive no 'chomp messages because nothing can eat them. ; When a fox receives a 'tick message, it examines the locations next to it. ; If one or more locations is occupied by a rabbit, it eats the rabbit and ; moves to that location. Its energy increases by rabbit-food-value. (define rabbit-food-value 10) ; If it finds another fox in that location, they fight and one of them has to ; die. The easiest way to implement this is to have the fox commit suicide ; when it finds another fox next to it. ; If none of the adjacent locations contains a rabbit or a fox, the fox moves to any ; adjacent location. It decrements its energy level by 1. ; Foxes are represented by blue circles instead of red circles. ; Fox breeding is controlled by the 'fox-breed-level variable. Birth-energy ; will be the same for foxes as for rabbits. (define fox-breed-level 100) ; YOUR ASSIGNMENT, PART 3: Implement the 'make-fox procedure. You may want ; to define one or two auxiliary functions analogous to next-spot to help the ; fox decide where to move to. Turn in a listing of your procedure(s). ; YOUR ASSIGNMENT, PART 4: Experiment with the system using ; different integer values for max-growth and the other global parameters. ; Turn in a very short report of your findings. In particular, what happens ; when rabbit-breed-level is made smaller (rabbits breed faster)? What happens ; when rabbit-breed-level is made larger (rabbits breed slower)? What seems to ; be the range of values for rabbit-breed-level and for rabbit-food-level for ; the animals to live indefinitely? Keep in mind that, due to the random ; function, the number of cycles that the animals survive will vary if you call ; run several times in a row without changing any parameters. ; GENERAL HINTS: For the message-passing paradigm, see pages 186-187. ; Note that in the grass example, I used a lambda expression rather ; than the explicitly named dispatch function in the book's example. ; (Ignore talk about the 'apply-generic procedure.) ; For objects with local state, see section 3.1.1, but note that in ; some ways, the code for make-grass is simpler than some of the ; examples in section 3.1.1. In particular, the grass and animal ; procedures never return procedures as a value as the example on page ; 223 does. Instead, they execute the appropriate code immediately ; and return either nothing, a symbol or a number. ; GENERAL HINT ABOUT MOVING ANIMALS: Both rabbits and foxes move to the place ; where their food was. If the square where a rabbit wants to move is occupied ; by grass, it must first chomp the grass, which will make the square empty, ; then it moves there. Similarly, if the square where a fox wants ; to move to is occupied by a rabbit, it chomps the rabbit first, then ; moves there. ; For both rabbits and foxes, an animal moves by first setting a local ; variable to itself, setting the square where it was in the world array ; to #f, and putting itself, in the new square. Thus, in outline, the ; code for moving the animal would look something like this: ; (let ((me (array-ref animal-world i j))) ; (vacate i j) ; some code goes here ; (array-set! animal-world ii jj me)) ; where ii, jj are the coordinates of the new location where the animal is ; moving to. You will also have to put in lines of code to ; draw the animal in the new square, and update its memory ; of where it is (i.e., the values of i and j). ; Auxiliary function for vacating a location (define (vacate i j) (array-set! animal-world i j #f) (let ((grass (array-ref grass-world i j))) (if (and grass (grass 'alive?)) (grass 'draw) (rectangle (make-posn (* o-size j) (* o-size i)) o-size o-size white)))) (define (make-rabbit i j energy) ; You supply definition here. ) (define (make-fox i j energy) ; You supply definition here. ;(run 100)