├── .gitignore ├── LICENSE ├── README.md ├── step1 ├── Game.hs ├── Readme.md └── Slime.bmp ├── step2 ├── Game.hs ├── Readme.md └── Slime.bmp ├── step3 ├── Game.hs ├── Readme.md └── Slime.bmp ├── step4 ├── Game.hs ├── Readme.md └── Slime.bmp ├── step5 ├── Game.hs ├── Readme.md ├── Slime.bmp ├── Slime2.bmp ├── Slime3.bmp └── Slime4.bmp ├── step6 ├── Game.hs ├── PowerUp.bmp ├── Readme.md ├── Slime.bmp ├── Slime2.bmp ├── Slime3.bmp ├── Slime4.bmp └── SuperSlime.bmp └── step7 ├── Game.hs ├── Platform.bmp ├── PowerUp.bmp ├── Readme.md ├── Slime.bmp ├── Slime2.bmp ├── Slime3.bmp ├── Slime4.bmp └── SuperSlime.bmp /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | cabal-dev 3 | *.o 4 | *.hi 5 | *.chi 6 | *.chs.h 7 | .virthualenv 8 | 9 | step1/Game 10 | step2/Game 11 | 12 | step3/Game 13 | 14 | step4/Game 15 | 16 | step5/Game 17 | 18 | step6/Game 19 | 20 | step7/Game 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, Manuel M T Chakravarty 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, this 11 | list of conditions and the following disclaimer in the documentation and/or 12 | other materials provided with the distribution. 13 | 14 | * Neither the name of the {organization} nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Let's program! 2 | ============== 3 | 4 | Here I'm collecting some notes and the code from introducing a group of children to programming by writing a simple game in Haskell. We are using [gloss-game](http://github.com/mchakravarty/gloss-game), a simple wrapper around the Gloss 2D library, to simplify the code. To draw sprites, we are using the pixel art editor BigPixel. An [iPad version of BigPixel](http://bigpixelapp.com) is available from the [App Store](https://itunes.apple.com/app/bigpixel-draw-pixel-art-sprites/id702704364). In addition, there is also a less polished, but free [cross-platform version of BigPixel](http://github.com/mchakravarty/BigPixel). 5 | 6 | So far, we had three coding sessions: 7 | 8 | 1. [Step1](https://github.com/mchakravarty/lets-program/tree/master/step1): Draw a simple character and move it around. 9 | 2. [Step2](https://github.com/mchakravarty/lets-program/tree/master/step2): Add gravity and bound character movement by window edges. 10 | 3. [Step3](https://github.com/mchakravarty/lets-program/tree/master/step3): Add a key binding to jump and let the character bounce when it hits the ground. 11 | 4. [Step4](https://github.com/mchakravarty/lets-program/tree/master/step4): Properly define constants, such as the window size, and implement continous moving when a movement key keeps being pressed. 12 | 5. [Step5](https://github.com/mchakravarty/lets-program/tree/master/step5): Use scenes to describe levels, and add an animation to the character when it jumps. 13 | 6. [Step6](https://github.com/mchakravarty/lets-program/tree/master/step6): Add a power up coin and let it super-charge the character. 14 | 7. [Step7](https://github.com/mchakravarty/lets-program/tree/master/step7): Use record syntax throughout for the game state and add a low platform. 15 | -------------------------------------------------------------------------------- /step1/Game.hs: -------------------------------------------------------------------------------- 1 | -- Compile this with 'ghc -o Game Game.hs' and run it with './Game'. 2 | 3 | import Graphics.Gloss.Game 4 | 5 | -- A sprite representing our character 6 | slimeSprite = bmp "Slime.bmp" 7 | 8 | -- Our game world consists purely of the location of our character. 9 | data World = World Point 10 | 11 | -- This starts our gamein a window with a give size, running at 30 frames per second. 12 | -- 13 | -- The argument 'World (0, 0)' is the initial state of our game world, where our character is at the centre of the 14 | -- window. 15 | -- 16 | main = play (InWindow "Slime is here!" (600, 400) (50, 50)) white 30 (World (0, 0)) draw handle [] 17 | 18 | -- To draw a frame, we position the character sprite at the location as determined by the current state of the world. 19 | -- We shrink the sprite by 50%. 20 | draw (World (x, y)) = translate x y (scale 0.5 0.5 slimeSprite) 21 | 22 | -- Whenever any of the keys 'a', 'd', 'w', or 's' have been pushed down, move our character in the corresponding 23 | -- direction. 24 | handle (EventKey (Char 'a') Down _ _) (World (x, y)) = World (x - 10, y) 25 | handle (EventKey (Char 'd') Down _ _) (World (x, y)) = World (x + 10, y) 26 | handle (EventKey (Char 'w') Down _ _) (World (x, y)) = World (x, y + 10) 27 | handle (EventKey (Char 's') Down _ _) (World (x, y)) = World (x, y - 10) 28 | handle event world = world -- don't change the world in case of any other events 29 | -------------------------------------------------------------------------------- /step1/Readme.md: -------------------------------------------------------------------------------- 1 | Time with a small group: 3h including setting up Haskell etc. 2 | 3 | We went through some preparatory steps: 4 | 5 | 1. Explain the basics of using a command shell: `pwd`, `ls`, `cd`, `mkdir`, `touch` 6 | 2. Launch `ghci`, do some simple arithmetic, lists, and some basic list operations. 7 | 3. Introduce the idea of variables with some simple `let` expression. 8 | 4. Introduce the idea of functions by defining simple functions in a `let` expression and using them. 9 | 10 | Then, create a file `Game.hs` and put the code for "Hello World!" into it: 11 | 12 | > main = putStrLn "Hello World!" 13 | 14 | Compile it: 15 | 16 | > ghc -o Game Game.hs 17 | 18 | Run it: 19 | 20 | > ./Game 21 | 22 | Then, the main part was to develop the version of `Game.hs` in this directory. This went as follows: 23 | 24 | 1. Everybody created a sprite for their character with [BigPixel](http://github.com/mchakravarty/BigPixel). 25 | 2. Then, they got shown the boilerplate to import `Graphics.Gloss.Game` and invoke the `play` function, doing nothing but calling `draw` (with the world value just being `()`). The function `draw` in turn just shows the sprite. 26 | 3. Talk about modifying Gloss pictures with `scale` and `translate` (maybe mention `rotate` as well). 27 | 4. Introduce the `World` datatype, initialise it in `play`, and use the character location in `draw`. Talk about how changing the initial value changes the drawn picture. 28 | 5. Add the event handler code to move the character. 29 | -------------------------------------------------------------------------------- /step1/Slime.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mchakravarty/lets-program/f0b636fd2f2b7db77b70c5208cad53c9fa493f44/step1/Slime.bmp -------------------------------------------------------------------------------- /step2/Game.hs: -------------------------------------------------------------------------------- 1 | -- Compile this with 'ghc -o Game Game.hs' and run it with './Game'. 2 | 3 | import Graphics.Gloss.Game 4 | 5 | -- A sprite representing our character 6 | slimeSprite = bmp "Slime.bmp" 7 | 8 | slimeWidth = 104 * 0.5 -- we draw it with scale 0.5 9 | slimeHeight = 104 * 0.5 10 | 11 | -- Our game world consists of both the location and the vertical velocity of our character. 12 | data World = World Point Float 13 | 14 | -- This starts our gamein a window with a give size, running at 30 frames per second. 15 | -- 16 | -- The argument 'World (0, 0) 0' is the initial state of our game world, where our character is at the centre of the 17 | -- window and has no velocity. 18 | -- 19 | main = play (InWindow "Slime is here!" (600, 400) (50, 50)) white 30 (World (0, 0) 0) draw handle [applyGravity] 20 | 21 | -- To draw a frame, we position the character sprite at the location as determined by the current state of the world. 22 | -- We shrink the sprite by 50%. 23 | draw (World (x, y) _v) = translate x y (scale 0.5 0.5 slimeSprite) 24 | 25 | -- Whenever any of the keys 'a', 'd', 'w', or 's' have been pushed down, move our character in the corresponding 26 | -- direction. 27 | handle (EventKey (Char 'a') Down _ _) (World (x, y) v) = World (moveX (x, y) (-10)) v 28 | handle (EventKey (Char 'd') Down _ _) (World (x, y) v) = World (moveX (x, y) 10) v 29 | handle (EventKey (Char 'w') Down _ _) (World (x, y) v) = World (moveY (x, y) 10) v 30 | handle (EventKey (Char 's') Down _ _) (World (x, y) v) = World (moveY (x, y) (-10)) v 31 | handle event world = world -- don't change the world in case of any other events 32 | 33 | -- Move horizontally, but box the character in at the window boundaries. 34 | moveX (x, y) offset = if x + offset < (-300) + slimeWidth / 2 35 | then ((-300) + slimeWidth / 2, y) 36 | else if x + offset > 300 - slimeWidth / 2 37 | then (300 - slimeWidth / 2, y) 38 | else (x + offset, y) 39 | 40 | -- Move vertically, but box the character in at the window boundaries. 41 | moveY (x, y) offset = if y + offset < (-200) + slimeHeight / 2 42 | then (x, (-200) + slimeHeight / 2) 43 | else if y + offset > 200 - slimeHeight / 2 44 | then (x, 200 - slimeHeight / 2) 45 | else (x, y + offset) 46 | 47 | -- Each frame, add the veclocity to the verticial position (y-axis) and decrease the velocity slightly. 48 | -- 49 | -- A negative velocity corresponds to a downward movement and by decreasing it continually, we accelerate downwards, 50 | -- which corresponds to a gravitational pull. 51 | -- 52 | applyGravity _time (World (x, y) v) = World (moveY (x, y) v) (v - 0.1) 53 | -------------------------------------------------------------------------------- /step2/Readme.md: -------------------------------------------------------------------------------- 1 | Time with a small group: 2.5h 2 | 3 | We started with a bit of revision: 4 | 5 | 1. Everybody created a new file `Readme.txt`. 6 | 2. Then, we went through all the shell commands we used so far (including launching `ghci` and compiling with `ghc`), while listing those commands with a brief description in `Readme.txt`, for later reference. 7 | 3. We talked about there being two types of declarations in our program: (1) function definitions and (2) a datatype definition. 8 | 4. We talked about what a function is and how functions receive data as arguments and then turn those arguments into different data or values. 9 | 5. We talked about `Int` versus `Float`. 10 | 6. We talked about the concept of a *pair*, specifically the `Point` type of Gloss, and how we can use pairs two combine two values into one and, then, use the functions `fst` and `snd` to take the pair apart again. (We evaluated a few expressions in `ghci` to illustrate that.) 11 | 12 | Next, we added gravity to our little game. 13 | 14 | 1. What is gravity? (Discussing locations, velocity, and acceleration.) 15 | 2. How does an animation work? (Frames, frames per second, Gloss can execute functions for us once per frame to advance the game world state.) 16 | 2. To add gravity to the game, we need to keep track of the vertical velocity of our character. We do that by extending the definition of `World` by adding a second component (of type `Float`). 17 | 3. Adapt the data definition and all functions that operate on the `World` type until everything works again. 18 | 4. We added a function `applyGravity` that adds the velocity to the vertical location. 19 | 5. To see what happens, we set the initial velocity to a value other than zero. (We discussed how that gives us a constant movement without acceleration.) 20 | 6. Then, we extended `applyGravity` to also decrease the velocity slightly (once per frame). 21 | 22 | As a consequence of gravity, the character leaves the screen quite quickly (and we can't recover it, as we can't press the up key quickly enough). This leads to the need to constrain the character to the window boundaries (or let it wrap around — we played around with both). 23 | 24 | 1. We started by talking about conditional expression and playing a bit with them in `ghci`. 25 | 2. Then, we used a single conditional to constrain the vertical position in `applyGravity`. 26 | 3. The initial idea is to test the verbatim character location against the window boundaries, but that leads to the character being cut off (as the location is in its middle). So, everybody needs to determine the size of their character (don't forget the scaling factor!) and adjust the boundary test correspondingly. 27 | 3. This still allows us to move the character of screen with the movement keys; hence, we need more conditionals. 28 | 4. To avoid repeating the location limiting code, we abstracted movement into two new functions `moveX` and `moveY`, which also ensure that we don't move beyond the window boundaries. 29 | 30 | -------------------------------------------------------------------------------- /step2/Slime.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mchakravarty/lets-program/f0b636fd2f2b7db77b70c5208cad53c9fa493f44/step2/Slime.bmp -------------------------------------------------------------------------------- /step3/Game.hs: -------------------------------------------------------------------------------- 1 | -- Compile this with 'ghc -o Game Game.hs' and run it with './Game'. 2 | 3 | import Graphics.Gloss.Game 4 | 5 | -- A sprite representing our character 6 | slimeSprite = bmp "Slime.bmp" 7 | 8 | slimeWidth = 104 * 0.5 -- we draw it with scale 0.5 9 | slimeHeight = 104 * 0.5 10 | 11 | -- Our game world consists of both the location and the vertical velocity of our character. 12 | data World = World Point Float 13 | 14 | -- This starts our gamein a window with a give size, running at 30 frames per second. 15 | -- 16 | -- The argument 'World (0, 0) 0' is the initial state of our game world, where our character is at the centre of the 17 | -- window and has no velocity. 18 | -- 19 | main 20 | = play (InWindow "Slime is here!" (600, 400) (50, 50)) white 30 (World (0, 0) 0) draw handle 21 | [applyVelocity, applyGravity] 22 | 23 | -- To draw a frame, we position the character sprite at the location as determined by the current state of the world. 24 | -- We shrink the sprite by 50%. 25 | draw (World (x, y) _v) = translate x y (scale 0.5 0.5 slimeSprite) 26 | 27 | -- Pressing 'a' and 'd' moves the character a fixed distance left or right. Pressing the spacebar makes it jump. 28 | handle (EventKey (Char 'a') Down _ _) (World (x, y) v) = World (moveX (x, y) (-10)) v 29 | handle (EventKey (Char 'd') Down _ _) (World (x, y) v) = World (moveX (x, y) 10) v 30 | handle (EventKey (SpecialKey KeySpace) Down _ _) (World (x, y) v) = World (x, y) 8 31 | handle event world = world -- don't change the world in case of any other events 32 | 33 | -- Move horizontally, but box the character in at the window boundaries. 34 | moveX (x, y) offset = if x + offset < (-300) + slimeWidth / 2 35 | then ((-300) + slimeWidth / 2, y) 36 | else if x + offset > 300 - slimeWidth / 2 37 | then (300 - slimeWidth / 2, y) 38 | else (x + offset, y) 39 | 40 | -- Move vertically, but box the character in at the window boundaries. 41 | moveY (x, y) offset = if y + offset < (-200) + slimeHeight / 2 42 | then (x, (-200) + slimeHeight / 2) 43 | else if y + offset > 200 - slimeHeight / 2 44 | then (x, 200 - slimeHeight / 2) 45 | else (x, y + offset) 46 | 47 | -- Each frame, add the veclocity to the verticial position (y-axis). (A negative velocity corresponds to a downward movement.) 48 | applyVelocity _time (World (x, y) v) = World (moveY (x, y) v) v 49 | 50 | -- We simulate gravity by decrease the velocity slightly on each frame, corresponding to a downward acceleration. 51 | -- 52 | -- We bounce of the bottom edge by reverting the velocity (with a damping factor). 53 | -- 54 | applyGravity _time (World (x, y) v) = World (x, y) (if y <= (-200) + slimeHeight / 2 then v * (-0.5) else v - 0.5) 55 | -------------------------------------------------------------------------------- /step3/Readme.md: -------------------------------------------------------------------------------- 1 | Time with a small group: 2.5h 2 | 3 | We spend quite a bit of time on revision (some children missed the second session): 4 | 5 | 1. We talked about how there are three components to programs: (1) functions, (2) data values, and (3) types. (We also put something about that into the `Readme.txt` file). 6 | 2. We went through the implementation of gravity and bounding the chracter movement at window boundaries again. We mostly did that by looking at the code of the children who were at the second session and, as far as possible, those children explaining how it worked (with prompts and help if they weren't sure anymore). 7 | 3. Step by step, the children who missed the second session added those features to their code as well. 8 | 4. We again looked at conditional expressions in GHCi again. (They and pattern matching are the most important control structures to understand at the moment.) 9 | 10 | Then, we added jumping to the game: 11 | 12 | 1. We talked about how we can realise jumping now that we track vertical velocity in the world. 13 | 2. We removed the cases in `handle` for up and down movement and added a case for a pressed down spacebar to jump. (Air jumps permitted for simplicity for the moment.) 14 | 15 | Finally, we added bouncing: 16 | 17 | 1. Instead of just stopping at the lower edge of the window, we talked about how we could make the character bounce. 18 | 2. It is tricky to avoid jittering when the character is on the "ground" in the original setup where `applyGravity` applies the current velocity to the vertical position and also increases the velocity to account for the gravitational force. 19 | 3. We solve that by splitting the function `applyGravity` into `applyVelocity` (movement) and `applyGravity` (account for gravitational force). This split enables us to reset the velocity to 0 when the character should stand still on the ground. 20 | -------------------------------------------------------------------------------- /step3/Slime.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mchakravarty/lets-program/f0b636fd2f2b7db77b70c5208cad53c9fa493f44/step3/Slime.bmp -------------------------------------------------------------------------------- /step4/Game.hs: -------------------------------------------------------------------------------- 1 | -- Compile this with 'ghc -o Game Game.hs' and run it with './Game'. 2 | 3 | import Data.List 4 | import Graphics.Gloss.Game 5 | 6 | -- Window size 7 | width = 600 8 | height = 400 9 | 10 | -- A sprite representing our character 11 | slimeSprite = bmp "Slime.bmp" 12 | 13 | slimeWidth = fst (snd (boundingBox (scale 0.5 0.5 slimeSprite))) 14 | slimeHeight = snd (snd (boundingBox (scale 0.5 0.5 slimeSprite))) 15 | 16 | -- Our game world consists of both the location and the vertical velocity of our character as well as a list of all 17 | -- currently pressed keys. 18 | data World = World Point Float [Char] 19 | 20 | -- This starts our gamein a window with a give size, running at 30 frames per second. 21 | -- 22 | -- The argument 'World (0, 0) 0' is the initial state of our game world, where our character is at the centre of the 23 | -- window and has no velocity. 24 | -- 25 | main 26 | = play (InWindow "Slime is here!" (round width, round height) (50, 50)) white 30 (World (0, 0) 0 []) draw handle 27 | [applyMovement, applyVelocity, applyGravity] 28 | 29 | -- To draw a frame, we position the character sprite at the location as determined by the current state of the world. 30 | -- We shrink the sprite by 50%. 31 | draw (World (x, y) _v _keys) = translate x y (scale 0.5 0.5 slimeSprite) 32 | 33 | -- Pressing the spacebar makes the character jump. All character keys are tracked in the world state. 34 | handle (EventKey (Char ch) Down _ _) (World (x, y) v keys) = World (x, y) v (ch : keys) 35 | handle (EventKey (Char ch) Up _ _) (World (x, y) v keys) = World (x, y) v (delete ch keys) 36 | handle (EventKey (SpecialKey KeySpace) Down _ _) (World (x, y) v keys) = World (x, y) 8 keys 37 | handle event world = world -- don't change the world in case of any other events 38 | 39 | -- Move horizontally, but box the character in at the window boundaries. 40 | moveX (x, y) offset = if x + offset < (-width / 2) + slimeWidth / 2 41 | then ((-width / 2) + slimeWidth / 2, y) 42 | else if x + offset > width / 2 - slimeWidth / 2 43 | then (width / 2 - slimeWidth / 2, y) 44 | else (x + offset, y) 45 | 46 | -- Move vertically, but box the character in at the window boundaries. 47 | moveY (x, y) offset = if y + offset < (-height / 2) + slimeHeight / 2 48 | then (x, (-height / 2) + slimeHeight / 2) 49 | else if y + offset > height / 2 - slimeHeight / 2 50 | then (x, height / 2 - slimeHeight / 2) 51 | else (x, y + offset) 52 | 53 | -- A pressed 'a' and 'd' key moves the character a fixed distance left or right. 54 | applyMovement _time (World (x, y) v keys) = if elem 'a' keys 55 | then World (moveX (x, y) (-10)) v keys 56 | else if elem 'd' keys 57 | then World (moveX (x, y) 10) v keys 58 | else World (x, y) v keys 59 | 60 | -- Each frame, add the veclocity to the verticial position (y-axis). (A negative velocity corresponds to a downward movement.) 61 | applyVelocity _time (World (x, y) v keys) = World (moveY (x, y) v) v keys 62 | 63 | -- We simulate gravity by decrease the velocity slightly on each frame, corresponding to a downward acceleration. 64 | -- 65 | -- We bounce of the bottom edge by reverting the velocity (with a damping factor). 66 | -- 67 | applyGravity _time (World (x, y) v keys) = World (x, y) (if y <= (-200) + slimeHeight / 2 then v * (-0.5) else v - 0.5) keys 68 | -------------------------------------------------------------------------------- /step4/Readme.md: -------------------------------------------------------------------------------- 1 | Time with a small group: 2.5h 2 | 3 | We did a little revision: 4 | 5 | * We discussed if-then-else expressions again and put a description into the `Readme.txt` file. 6 | 7 | Next we talked about how hardcoding values (such as the size of the game window) in multiple places makes it hard to change those values. 8 | 9 | 1. We defined `width` and `height` as top-level constants and changed the rest of the code to use those. 10 | 2. We talked again about the difference of `Int` and `Float` and why we need to use `round` in the window size argument to `play`. (I let them see the error first, before talking about this and also explored this a bit in GHCi.) 11 | 3. We defined `spriteWidth` and `spriteHeight` as top-level constants computed from the result of calling gloss-game's `boundingBox` on the (scaled) sprite. (Again, GHCi is good to evaluate some expressions involving the new function, to get a feel for it.) 12 | 13 | In a game, we want to be able to keep pressing the movement keys ('a' and 'd') to keep the character moving. However, in the previous implementation, we had to repeatedly press to continue advancing. The solution, in Gloss' event handling, is to explicitly keep track of pressed keys in the game world state: 14 | 15 | 1. Introduce a third component in `World` that keeps track of a list of pressed keys (of type `[Char]`). 16 | 2. Change `handle` such that it tracks `Down` and `Up` events for all `Char` keys, and use them to add and remove keys from the list of pressed keys. (NB: In my exprience, Gloss can lose events. It is bad if that happens with an `Up` event. Hence, it would be better to `filter` characters out when an `Up` event occurs — instead just using `delete`. However, I didn't want to introduce the complexity of `filter` just because of this.) 17 | 3. Add a new function `applyMovement` (that's also being put into the list of stepper functions passed to `play`). 18 | 4. The function `applyMovement` tests for the movement keys and calls `moveX` as needed. 19 | -------------------------------------------------------------------------------- /step4/Slime.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mchakravarty/lets-program/f0b636fd2f2b7db77b70c5208cad53c9fa493f44/step4/Slime.bmp -------------------------------------------------------------------------------- /step5/Game.hs: -------------------------------------------------------------------------------- 1 | -- Compile this with 'ghc -o Game Game.hs' and run it with './Game'. 2 | 3 | import Data.List 4 | import Graphics.Gloss.Game 5 | 6 | -- Window size 7 | width = 600 8 | height = 400 9 | 10 | -- A sprite representing our character 11 | slimeSprite = scale 0.5 0.5 (bmp "Slime.bmp") 12 | 13 | slimeWidth = fst (snd (boundingBox slimeSprite)) 14 | slimeHeight = snd (snd (boundingBox slimeSprite)) 15 | 16 | -- Additional sprites for a simple animation 17 | slimeSprite2 = scale 0.5 0.5 (bmp "Slime2.bmp") 18 | slimeSprite3 = scale 0.5 0.5 (bmp "Slime3.bmp") 19 | slimeSprite4 = scale 0.5 0.5 (bmp "Slime4.bmp") 20 | 21 | 22 | -- Our game world consists of both the location and the vertical velocity of our character, the state of the character 23 | -- animation as well as a list of all currently pressed keys. 24 | data World = World Point Float Animation [Char] 25 | 26 | -- This starts our gamein a window with a give size, running at 30 frames per second. 27 | -- 28 | -- The argument 'World (0, 0) 0' is the initial state of our game world, where our character is at the centre of the 29 | -- window and has no velocity. 30 | -- 31 | main 32 | = playInScene (InWindow "Slime is here!" (round width, round height) (50, 50)) white 30 33 | (World (0, 0) 0 noAnimation []) level handle 34 | [applyMovement, applyVelocity, applyGravity] 35 | 36 | -- Description of a level as a scene. Scenes depend on the state of the world, which means their rendering changes as the 37 | -- world changes. Here, we have the character, where both its location and animation depend on the world state. 38 | level = translating spritePosition (animating spriteAnimation slimeSprite) 39 | 40 | -- Extract the character position from the world. 41 | spritePosition (World (x, y) v anim keys) = (x, y) 42 | 43 | -- Extract the character animation from the world. 44 | spriteAnimation (World (x, y) v anim keys) = anim 45 | 46 | -- Pressing the spacebar makes the character jump. All character keys are tracked in the world state. 47 | handle now (EventKey (Char ch) Down _ _) (World (x, y) v anim keys) = World (x, y) v anim (ch : keys) 48 | handle now (EventKey (Char ch) Up _ _) (World (x, y) v anim keys) = World (x, y) v anim (delete ch keys) 49 | handle now (EventKey (SpecialKey KeySpace) Down _ _) (World (x, y) v anim keys) = 50 | World (x, y) 8 (animation [slimeSprite2, slimeSprite3, slimeSprite4] 0.1 now) keys 51 | handle now event world = world -- don't change the world in case of any other events 52 | 53 | -- Move horizontally, but box the character in at the window boundaries. 54 | moveX (x, y) offset = if x + offset < (-width / 2) + slimeWidth / 2 55 | then ((-width / 2) + slimeWidth / 2, y) 56 | else if x + offset > width / 2 - slimeWidth / 2 57 | then (width / 2 - slimeWidth / 2, y) 58 | else (x + offset, y) 59 | 60 | -- Move vertically, but box the character in at the window boundaries. 61 | moveY (x, y) offset = if y + offset < (-height / 2) + slimeHeight / 2 62 | then (x, (-height / 2) + slimeHeight / 2) 63 | else if y + offset > height / 2 - slimeHeight / 2 64 | then (x, height / 2 - slimeHeight / 2) 65 | else (x, y + offset) 66 | 67 | -- A pressed 'a' and 'd' key moves the character a fixed distance left or right. 68 | applyMovement _now _time (World (x, y) v anim keys) = if elem 'a' keys 69 | then World (moveX (x, y) (-10)) v anim keys 70 | else if elem 'd' keys 71 | then World (moveX (x, y) 10) v anim keys 72 | else World (x, y) v anim keys 73 | 74 | -- Each frame, add the veclocity to the verticial position (y-axis). (A negative velocity corresponds to a downward movement.) 75 | applyVelocity _now _time (World (x, y) v anim keys) = World (moveY (x, y) v) v anim keys 76 | 77 | -- We simulate gravity by decrease the velocity slightly on each frame, corresponding to a downward acceleration. 78 | -- 79 | -- We bounce of the bottom edge by reverting the velocity (with a damping factor). 80 | -- 81 | applyGravity _now _time (World (x, y) v anim keys) = 82 | World (x, y) (if y <= (-200) + slimeHeight / 2 then v * (-0.5) else v - 0.5) anim keys 83 | -------------------------------------------------------------------------------- /step5/Readme.md: -------------------------------------------------------------------------------- 1 | Time with a small group: 2.5h 2 | 3 | We spent some time on a little revision bringing a group member who missed the last two sessions up to speed. 4 | 5 | We talked about the concept of scenes as parameterised pictures and how we can use that to implement our moving character differently and also animate it. In a first step, we rewrote the program to use scenes without changing the behaviour. 6 | 7 | 1. Replace the use of the `play` functions with `playInScene`. 8 | 2. Replace the `draw` functions with a `level` definition (that for now omits the animation). 9 | 3. Implement a projection function `spritePosition` that extracts just the position of the character from the `World`. 10 | 3. The event handler and the stepper functions need to be extended to receive an additional first argument (being the current time since invoking `playInScene`. 11 | 12 | In the next step, we add the capability of being able to animate the character. 13 | 14 | 1. Extend `level` to use `animating` (so we can animate the character). 15 | 2. To this end, we also need to add a forth component, of type `Animation` to the `World`, which stores the current animation state for that animation. 16 | 3. We extend the initial `World` argument to `playInScene` to start with `noAnimation` for the extra component. 17 | 4. Implement a projection function `spriteAnimation` to extract the new component from the `World`. 18 | 19 | Finally, everybody drew three more sprites as variations on their character sprite to form an animation sequence. 20 | 21 | 1. Define the new sprites in top-level bindings. 22 | 2. In the event handling case for spacebar (implementing jumping), we also start an animation consisting of the three new sprites. 23 | -------------------------------------------------------------------------------- /step5/Slime.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mchakravarty/lets-program/f0b636fd2f2b7db77b70c5208cad53c9fa493f44/step5/Slime.bmp -------------------------------------------------------------------------------- /step5/Slime2.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mchakravarty/lets-program/f0b636fd2f2b7db77b70c5208cad53c9fa493f44/step5/Slime2.bmp -------------------------------------------------------------------------------- /step5/Slime3.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mchakravarty/lets-program/f0b636fd2f2b7db77b70c5208cad53c9fa493f44/step5/Slime3.bmp -------------------------------------------------------------------------------- /step5/Slime4.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mchakravarty/lets-program/f0b636fd2f2b7db77b70c5208cad53c9fa493f44/step5/Slime4.bmp -------------------------------------------------------------------------------- /step6/Game.hs: -------------------------------------------------------------------------------- 1 | -- Compile this with 'ghc -o Game Game.hs' and run it with './Game'. 2 | 3 | import Data.List 4 | import Graphics.Gloss.Game 5 | 6 | -- Window size 7 | width = 600 8 | height = 400 9 | 10 | -- A sprite representing our character 11 | slimeSprite = scale 0.5 0.5 (bmp "Slime.bmp") 12 | 13 | slimeWidth = fst (snd (boundingBox slimeSprite)) 14 | slimeHeight = snd (snd (boundingBox slimeSprite)) 15 | 16 | -- Additional sprites for a simple animation 17 | slimeSprite2 = scale 0.5 0.5 (bmp "Slime2.bmp") 18 | slimeSprite3 = scale 0.5 0.5 (bmp "Slime3.bmp") 19 | slimeSprite4 = scale 0.5 0.5 (bmp "Slime4.bmp") 20 | 21 | -- Sprite for the power up coin 22 | powerUpSprite = scale 0.5 0.5 (bmp "PowerUp.bmp") 23 | 24 | powerUpWidth = fst (snd (boundingBox powerUpSprite)) 25 | powerUpHeight = snd (snd (boundingBox powerUpSprite)) 26 | 27 | -- A sprite of our character after powerup 28 | superSlimeSprite = scale 0.5 0.5 (bmp "SuperSlime.bmp") 29 | 30 | -- Our game world consists of all the variable aspects of the game state as well as a list of all currently pressed keys. 31 | data World = World { 32 | spritePosition :: Point, 33 | spriteVelocity :: Float, 34 | spriteAnimation :: Animation, 35 | powerUpPosition :: Point, 36 | keys :: [Char] 37 | } 38 | 39 | -- This starts our gamein a window with a give size, running at 30 frames per second. 40 | -- 41 | -- The argument 'World (0, 0) 0' is the initial state of our game world, where our character is at the centre of the 42 | -- window and has no velocity. 43 | -- 44 | main 45 | = playInScene (InWindow "Slime is here!" (round width, round height) (50, 50)) white 30 46 | (World (0, 0) 0 noAnimation (100, 100) []) level handle 47 | [applyMovement, applyVelocity, applyGravity, checkForPowerUp] 48 | 49 | -- Description of a level as a scene. Scenes depend on the state of the world, which means their rendering changes as the 50 | -- world changes. Here, we have the character, where both its location and animation depend on the world state. 51 | level = scenes [translating spritePosition (animating spriteAnimation slimeSprite), 52 | translating powerUpPosition (picture powerUpSprite)] 53 | 54 | -- Pressing the spacebar makes the character jump. All character keys are tracked in the world state. 55 | handle now (EventKey (Char ch) Down _ _) (World (x, y) v anim puPos keys) = World (x, y) v anim puPos (ch : keys) 56 | handle now (EventKey (Char ch) Up _ _) (World (x, y) v anim puPos keys) = World (x, y) v anim puPos (delete ch keys) 57 | handle now (EventKey (SpecialKey KeySpace) Down _ _) (World (x, y) v anim puPos keys) = 58 | World (x, y) 8 (animation [slimeSprite2, slimeSprite3, slimeSprite4] 0.1 now) puPos keys 59 | handle now event world = world -- don't change the world in case of any other events 60 | 61 | -- Move horizontally, but box the character in at the window boundaries. 62 | moveX (x, y) offset = if x + offset < (-width / 2) + slimeWidth / 2 63 | then ((-width / 2) + slimeWidth / 2, y) 64 | else if x + offset > width / 2 - slimeWidth / 2 65 | then (width / 2 - slimeWidth / 2, y) 66 | else (x + offset, y) 67 | 68 | -- Move vertically, but box the character in at the window boundaries. 69 | moveY (x, y) offset = if y + offset < (-height / 2) + slimeHeight / 2 70 | then (x, (-height / 2) + slimeHeight / 2) 71 | else if y + offset > height / 2 - slimeHeight / 2 72 | then (x, height / 2 - slimeHeight / 2) 73 | else (x, y + offset) 74 | 75 | -- A pressed 'a' and 'd' key moves the character a fixed distance left or right. 76 | applyMovement _now _time (World (x, y) v anim puPos keys) 77 | = if elem 'a' keys 78 | then World (moveX (x, y) (-10)) v anim puPos keys 79 | else if elem 'd' keys 80 | then World (moveX (x, y) 10) v anim puPos keys 81 | else World (x, y) v anim puPos keys 82 | 83 | -- Each frame, add the veclocity to the verticial position (y-axis). (A negative velocity corresponds to a downward movement.) 84 | applyVelocity _now _time (World (x, y) v anim puPos keys) = World (moveY (x, y) v) v anim puPos keys 85 | 86 | -- We simulate gravity by decrease the velocity slightly on each frame, corresponding to a downward acceleration. 87 | -- 88 | -- We bounce of the bottom edge by reverting the velocity (with a damping factor). 89 | -- 90 | applyGravity _now _time (World (x, y) v anim puPos keys) = 91 | World (x, y) (if y <= (-200) + slimeHeight / 2 then v * (-0.5) else v - 0.5) anim puPos keys 92 | 93 | -- Check whether the character and the power sprite intersect. Then, change the character sprite for 3s to a super version. 94 | checkForPowerUp now _time (World (x, y) v anim (pux, puy) keys) 95 | = if abs (x - pux) < slimeWidth / 2 + powerUpWidth / 2 && abs (y - puy) < slimeHeight / 2 + powerUpHeight / 2 96 | then World (x, y) v (animation [superSlimeSprite] 3 now) (-pux, -puy) keys 97 | else World (x, y) v anim (pux, puy) keys -------------------------------------------------------------------------------- /step6/PowerUp.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mchakravarty/lets-program/f0b636fd2f2b7db77b70c5208cad53c9fa493f44/step6/PowerUp.bmp -------------------------------------------------------------------------------- /step6/Readme.md: -------------------------------------------------------------------------------- 1 | Time with a small group: 2.5h 2 | 3 | In this session, we added a power up coin: 4 | 5 | 1. We started by extending the scene in `level` with a power up coin and drawing a coin sprite. Initially, the coin was at a fixed static position. 6 | 2. We talked about how to check for whether the character touches the power up coin. 7 | 3. Then, we added a new stepper function `checkForPowerUp` that checks for the intersection between the coin and the character. 8 | 4. When the character touches the power up, it temporarily changes into a supercharged version (for which everybody drew a new sprite). 9 | 10 | To be able to change the position of the coin, we need to extend the `World` and, to add that to the scene, also need a new function that projects the coin position out of a `World` state. We talked about how the growing `World` data type becomes unwieldy. This discussion led to introducing the Haskell record syntax (for now only for projections, not yet for updates). 11 | 12 | 1. Change the definition of `World` to use record syntax. 13 | 2. Add a new field `powerUpPosition` for the position of the power up coin. 14 | 3. Adapt the rest of the program to that change of `World`. 15 | 16 | Finally, we use the extended `World` to change the coin position whenever it is used to power up the character: 17 | 18 | 1. Edit the scene to use `powerUpPosition` for the coin position. 19 | 2. In `checkForPowerUp`, alter the coin position by changing the sign of the x and y component. 20 | -------------------------------------------------------------------------------- /step6/Slime.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mchakravarty/lets-program/f0b636fd2f2b7db77b70c5208cad53c9fa493f44/step6/Slime.bmp -------------------------------------------------------------------------------- /step6/Slime2.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mchakravarty/lets-program/f0b636fd2f2b7db77b70c5208cad53c9fa493f44/step6/Slime2.bmp -------------------------------------------------------------------------------- /step6/Slime3.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mchakravarty/lets-program/f0b636fd2f2b7db77b70c5208cad53c9fa493f44/step6/Slime3.bmp -------------------------------------------------------------------------------- /step6/Slime4.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mchakravarty/lets-program/f0b636fd2f2b7db77b70c5208cad53c9fa493f44/step6/Slime4.bmp -------------------------------------------------------------------------------- /step6/SuperSlime.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mchakravarty/lets-program/f0b636fd2f2b7db77b70c5208cad53c9fa493f44/step6/SuperSlime.bmp -------------------------------------------------------------------------------- /step7/Game.hs: -------------------------------------------------------------------------------- 1 | -- Compile this with 'ghc -o Game Game.hs' and run it with './Game'. 2 | 3 | import Data.List 4 | import Graphics.Gloss.Game 5 | 6 | -- Window size 7 | width = 600 8 | height = 400 9 | 10 | -- A sprite representing our character 11 | slimeSprite = scale 0.5 0.5 (bmp "Slime.bmp") 12 | 13 | slimeWidth = fst (snd (boundingBox slimeSprite)) 14 | slimeHeight = snd (snd (boundingBox slimeSprite)) 15 | 16 | -- Additional sprites for a simple animation 17 | slimeSprite2 = scale 0.5 0.5 (bmp "Slime2.bmp") 18 | slimeSprite3 = scale 0.5 0.5 (bmp "Slime3.bmp") 19 | slimeSprite4 = scale 0.5 0.5 (bmp "Slime4.bmp") 20 | 21 | -- Sprite for the power up coin 22 | powerUpSprite = scale 0.5 0.5 (bmp "PowerUp.bmp") 23 | 24 | powerUpWidth = fst (snd (boundingBox powerUpSprite)) 25 | powerUpHeight = snd (snd (boundingBox powerUpSprite)) 26 | 27 | -- A sprite of our character after powerup 28 | superSlimeSprite = scale 0.5 0.5 (bmp "SuperSlime.bmp") 29 | 30 | -- A sprite for a platform to jump onto 31 | platformSprite = scale 0.5 0.5 (bmp "Platform.bmp") 32 | 33 | platformPosition = (-200, -150) -- It doesn't change, so it doesn't need to go into the 'World' 34 | 35 | platformWidth = fst (snd (boundingBox platformSprite)) 36 | platformHeight = snd (snd (boundingBox platformSprite)) 37 | 38 | 39 | -- Our game world consists of all the variable aspects of the game state as well as a list of all currently pressed keys. 40 | data World = World { 41 | spritePosition :: Point, 42 | spriteVelocity :: Float, 43 | spriteAnimation :: Animation, 44 | powerUpPosition :: Point, 45 | keys :: [Char] 46 | } 47 | 48 | -- This starts our gamein a window with a give size, running at 30 frames per second. 49 | -- 50 | -- The argument 'World (0, 0) 0' is the initial state of our game world, where our character is at the centre of the 51 | -- window and has no velocity. 52 | -- 53 | main 54 | = playInScene (InWindow "Slime is here!" (round width, round height) (50, 50)) white 30 55 | (World (0, 0) 0 noAnimation (100, 100) []) level handle 56 | [applyMovement, applyVelocity, applyGravity, checkForPowerUp] 57 | 58 | -- Description of a level as a scene. Scenes depend on the state of the world, which means their rendering changes as the 59 | -- world changes. Here, we have the character, where both its location and animation depend on the world state. 60 | level = scenes [translating spritePosition (animating spriteAnimation slimeSprite), 61 | translating powerUpPosition (picture powerUpSprite), 62 | picture (translate (fst platformPosition) (snd platformPosition) platformSprite) ] 63 | 64 | -- Pressing the spacebar makes the character jump. All character keys are tracked in the world state. 65 | handle now (EventKey (Char ch) Down _ _) world = world { keys = ch : keys world } 66 | handle now (EventKey (Char ch) Up _ _) world = world { keys = delete ch (keys world) } 67 | handle now (EventKey (SpecialKey KeySpace) Down _ _) world = 68 | world { spriteVelocity = 8, 69 | spriteAnimation = animation [slimeSprite2, slimeSprite3, slimeSprite4] 0.1 now } 70 | handle now event world = world -- don't change the world in case of any other events 71 | 72 | -- Move horizontally, but box the character in at the window boundaries. 73 | moveX (x, y) offset = if x + offset < (-width / 2) + slimeWidth / 2 74 | then ((-width / 2) + slimeWidth / 2, y) 75 | else if x + offset > width / 2 - slimeWidth / 2 76 | then (width / 2 - slimeWidth / 2, y) 77 | else (x + offset, y) 78 | 79 | -- Move vertically, but box the character in at the window boundaries. Also check for collision with the platform, 80 | -- which will also have to stop the character. 81 | moveY (x, y) offset = if y + offset < (-height / 2) + slimeHeight / 2 82 | then (x, (-height / 2) + slimeHeight / 2) 83 | else if y + offset > height / 2 - slimeHeight / 2 84 | then (x, height / 2 - slimeHeight / 2) 85 | else if y + offset < snd platformPosition + slimeHeight / 2 + platformHeight / 2 86 | && x + slimeWidth / 2 > fst platformPosition - platformWidth / 2 87 | && x - slimeWidth / 2 < fst platformPosition + platformWidth / 2 88 | then (x, snd platformPosition + slimeHeight / 2 + platformHeight / 2) 89 | else (x, y + offset) 90 | 91 | -- A pressed 'a' and 'd' key moves the character a fixed distance left or right. 92 | applyMovement _now _time world 93 | = if elem 'a' (keys world) 94 | then world { spritePosition = moveX (spritePosition world) (-10) } 95 | else if elem 'd' (keys world) 96 | then world { spritePosition = moveX (spritePosition world) 10 } 97 | else world 98 | 99 | -- Each frame, add the veclocity to the verticial position (y-axis). (A negative velocity corresponds to a downward movement.) 100 | applyVelocity _now _time world = world { spritePosition = moveY (spritePosition world) (spriteVelocity world) } 101 | 102 | -- We simulate gravity by decrease the velocity slightly on each frame, corresponding to a downward acceleration. 103 | -- 104 | -- We bounce of the bottom edge by reverting the velocity (with a damping factor). 105 | -- 106 | applyGravity _now _time world 107 | = let (x, y) = spritePosition world 108 | v = spriteVelocity world 109 | in 110 | world { spriteVelocity = if onTheFloor world || onThePlatform world then v * (-0.5) else v - 0.5 } 111 | 112 | -- Check whether the character is on the floor. 113 | onTheFloor world = snd (spritePosition world) <= (-height / 2) + slimeHeight / 2 114 | 115 | -- Check whether the character is on the playform. 116 | onThePlatform world = snd (spritePosition world) <= snd platformPosition + slimeHeight / 2 + platformHeight / 2 117 | && fst (spritePosition world) + slimeWidth / 2 > fst platformPosition - platformWidth / 2 118 | && fst (spritePosition world) - slimeWidth / 2 < fst platformPosition + platformWidth / 2 119 | 120 | -- Check whether the character and the power sprite intersect. Then, change the character sprite for 3s to a super version. 121 | checkForPowerUp now _time world 122 | = let (x, y) = spritePosition world 123 | (pux, puy) = powerUpPosition world 124 | in 125 | if abs (x - pux) < slimeWidth / 2 + powerUpWidth / 2 && abs (y - puy) < slimeHeight / 2 + powerUpHeight / 2 126 | then world { spriteAnimation = animation [superSlimeSprite] 3 now, powerUpPosition = (-pux, -puy) } 127 | else world 128 | -------------------------------------------------------------------------------- /step7/Platform.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mchakravarty/lets-program/f0b636fd2f2b7db77b70c5208cad53c9fa493f44/step7/Platform.bmp -------------------------------------------------------------------------------- /step7/PowerUp.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mchakravarty/lets-program/f0b636fd2f2b7db77b70c5208cad53c9fa493f44/step7/PowerUp.bmp -------------------------------------------------------------------------------- /step7/Readme.md: -------------------------------------------------------------------------------- 1 | Time with a small group: 2.5h 2 | 3 | We spent quite some time getting some children who couldn't make the last two sessions up to speed. To make that interesting for the others, we started by revising and further exploring the record syntax for the `World` datatype: 4 | 5 | 1. Make sure everybody uses record syntax for `World`. 6 | 2. Talk about the advantages of naming the fields and explore both projections as well as updates using record syntax in GHCi (at the example of the `World` datatype). (I on purpose omitted pattern matching with record syntax. It would have been to much.) 7 | 3. Then, everybody (with some help) rewrites their code to use record updates and record projections instead of pattern matching on the `World` constructor and reconstructing an entire `World` value in the various event handler and stepper functions. 8 | 9 | Extend the scene by a low platform: 10 | 11 | 1. Draw a platform sprite. 12 | 2. Add it to the scene (it's static). 13 | 3. Extend the movement and gravity code to check for collisions of the character with the platform and handle them approrpiately. 14 | -------------------------------------------------------------------------------- /step7/Slime.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mchakravarty/lets-program/f0b636fd2f2b7db77b70c5208cad53c9fa493f44/step7/Slime.bmp -------------------------------------------------------------------------------- /step7/Slime2.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mchakravarty/lets-program/f0b636fd2f2b7db77b70c5208cad53c9fa493f44/step7/Slime2.bmp -------------------------------------------------------------------------------- /step7/Slime3.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mchakravarty/lets-program/f0b636fd2f2b7db77b70c5208cad53c9fa493f44/step7/Slime3.bmp -------------------------------------------------------------------------------- /step7/Slime4.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mchakravarty/lets-program/f0b636fd2f2b7db77b70c5208cad53c9fa493f44/step7/Slime4.bmp -------------------------------------------------------------------------------- /step7/SuperSlime.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mchakravarty/lets-program/f0b636fd2f2b7db77b70c5208cad53c9fa493f44/step7/SuperSlime.bmp --------------------------------------------------------------------------------