programming编程辅导、Python程序设计讲解、辅导Python语言编程
- 首页 >> Python编程 Organization and Scope
While there are no test cases this time, you should be able to figure
out if everything is working simply by playing the game. There are
no tricky “restore everything to how it was” like with Turtles; no
nasty surprises lurking in the specifications. Just get the game
working.
Assignment Source Code
To work on this assignment, you will need all of the following:
The last item is a link that you should refer to through the
assignment, while the other two are files to download. Only the first
is a must download, as it contains the all of the source code
necessary to complete the assignment.
The second file is a collection of demo code from the lesson
on GUI programming, as well as the lesson on coroutines. This
sample code contains a lot of hints on how to approach some of
the harder parts of this assignment, and we reference these
samples throughout the instructions.
As with the imager application, this assignment is organized as a
package with several files. To run the application, change the
directory in your command shell to just outside of the
folder froggit and type
File Description
froggit.zip The application package, with all the source code
samples.zip Several programs that give hints on this assignment
game2d API The documentation for how to use the game2d classes
python froggit
In this case, Python will run the entire folder. What this really means
is that it runs the script in __main__.py. This script imports each of the
other modules in this folder to create a complex application. To
work properly, the froggit folder should contain the following:
For the most part, you only need to understand the first four files -
app.py, level.py, lanes.py and models.py - as well as the JSON directory.
The other files and folders can be ignored. However, if you decide
to add a few extensions, it helps to understand how all of these fit
together.
app.py
This module contains the controller class Froggit. This is the
controller that launches the application, and is one of four modules
that you must modify for this assignment. While it is the primary
controller class, you will note that it has no script code. That is
File Description
app.py The primary controller class for the application
level.py A secondary controller for a single playable application
lanes.py A collection of mini-controllers for the individual lanes
models.py Model classes for the game (i.e. Frog)
consts.py All module with all of the constant (global variable) values
game2d A package with classes that can display graphics on the screen
Sounds A folder of sound effects approved for your use
Fonts A folder of True Type fonts approved for your use
Images A folder of images for the frog and various obstacles
JSON A folder with different levels you can play
contained in the module __main__.py (which you should not
modify).
level.py
This module contains the secondary controller class Level. This
class manages a single level of the game. It works as a
subcontroller, just like the example subcontroller.py in the provided
sample code. It is another of the four modules that you must
modify for this assignment, and is the one that will require the most
original code.
lanes.py
Levels in Frogger consist of horizontal lanes that the frog has to
traverse. These could be roads, water, or even just a strip of grass.
And each of these lanes is very different from one another. The frog
does not die from touching a road, but will die if it is hit by a car on
the road. On the other hand, the frog dies in the water (it cannot
swim) unless it is touching a log.
This will make the code quite complicated. So to make things
much easier, you will make a class for each type of lane: road,
water, grass and (exit) hedge. Technically these classes will act as
subcontrollers to Level just as Level is a subcontroller for Froggit. In
complex applications there are many layers of controllers.
models.py
This module contains the model class Frog. This class is similar to
those found in the pyro.py demo in the provided sample code. If you
want to add other model classes (e.g. turtles or pick-ups), then you
should add those here as well. This is the last of the four files you
must modify for this assignment.
consts.py
This module is filled with constants (global variables that should
not ever change). It is used
by app.py, level.py, lanes.py and models.py to ensure that these
modules agree on certain important values. It also contains code
for adjusting your default level. You should only modify this file if
you are adding additional features to your program.
game2d
This is a package containing the classes you will use to design
your game. These are classes that you will subclass, just like we
demonstrated in the lesson videos. In particular, the class Froggit is
a subclass of GameApp from this package. As part of this
assignment, you are expected to read the online
documentation which describes how to use the base classes.
Under no circumstances should you ever modify this package!
Sounds
This is a folder of sound effects that you will use in the final
activity. You are also free to add more if you wish; just put them in
this folder. All sounds must be WAV files. While we have gotten
MP3 to work on Windows, Python support for MP3 on MacOS is
unreliable.
Fonts
This is a folder of True Type Fonts, should you get tired of the
default font for this assignment. You can put whatever font you
want in this folder, provided it is a .ttf file. Other Font formats (such
as .ttc, .otf, or .dfont) are not supported. Be very careful with fonts,
however, as they are copyrighted in the same way images are. Do
not assume that you can include any font that you find on your
computer.
Images
This is a folder with image files for the frog and the obstacles.
The GImage and GSprite classes allow you to include these in
your game. You can put other images here if you wish.
JSON
This is a folder with JSON files that define a level. You will turn
these into dictionaries and use them to place objects in your game.
Understanding these files will be a major portion of this
assignment. But you should look at them briefly to familiarize
yourself with these files.
Assignment Scope
As we explained in class, your game is a subclass of GameApp.
The parent class does a lot of work for you. You just need to
implement three main methods. They are as follows:
Your goal is to implement all of these methods according to their
(provided) specification.
Method Description
start() Method to initialize the game state attributes
update(dt) Method to update the models for the next animation frame
draw() Method to draw all models to the screen
start()
This method should take the place of __init__. Because of how Kivy
works, initialization code should go here and not in the initializer
(which is called before the window is sized properly).
update(dt)
This method should move the position of everything for just one
animation step, and resolve any collisions (potentially deleting
objects). The speed at which this method is called is determined by
the (immutable) attribute fps, which is set by the constructor. The
parameter dt is time in seconds since the last call to update.
draw()
This method is called as soon as update is complete. Implementing
this method should be as simple as calling the method draw,
inherited from GObject, on each of the models.
These are the only three methods that you need to implement. But
obviously you are not going to put all of your code in those three
methods. The result would be an unreadable mess. An important
part of this assignment is developing new (helper) methods
whenever you need them so that each method is small and
manageable. Your grade will depend partly on the design of your
program. As one guideline, points will be deducted for methods
that are more than 30 lines long (not including specifications or
spacing).
You will also need to add methods and attributes to the
class Level in level.py, the class Frog in models.py and the
classes Lane, Road, Water. and Hedge in lanes.py. All of these classes are
completely empty, though we have given you a lot of hints in the
class specification. You should read all these specifications.
As you write the assignment, you may find that you need additional
attributes. All new instance attributes should be hidden. You should
list these new attributes and their invariants as single-line
comments after the class specification (as we have done this
semester). For example, if the Level class needs to access the exit
locations in a Hedge lane, then you are going to need a getter for
these exits (and we give hints on how to do this).
While documentation and encapsulation is essential for this
assignment, you do not need to enforce any preconditions or
invariants. However, you may find that debugging is a lot simpler if
you do, as it will help you localize errors. But we will not take off
points either way, as long as the game runs correctly.
Assignment Organization
This assignment follows the model-view-controller pattern
discussed in the lesson videos. The modules are clearly organized
so that each holds models, the view, or a controller. The
organization of these files is shown below. The arrows in this
diagram mean “accesses”. So the Froggit controller accesses the
view and Level subcontroller. The Level controller accesses the
lanes, the view, and the models. And Lane and its subclasses
access the view and the modesl.
This leads to an important separation of files. Froggit is never
permitted to access anything in models.py and lanes.py. Level is never
permitted to access anything in app.py. The classes in lanes.py may
not access anythign in app.py or level.py. And finally, models.py cannot
access anything other than the view. This is an important rule that
we will enforce while grading. All of this is shown in the diagram
below
The Import Relationships
In addition to the four main modules, there is another module with
no class or function definitions. It only has constants, which are
global variables that do not change. This is imported by
the models module and the various controllers. It is a way to help
them share information.
When approaching this assignment, you should always be thinking
about “what code goes where?” If you do not know what file to put
things in, please ask on Piazza (but do not post code). Here are
some rough guidelines.
Froggit
This controller does very little. All it does is keep track of the game
state (e.g. whether or not the game is paused). Most of the time it
just calls the methods in Level, and lets Level do all the work.
However, if you need anything between lives, like a paused
message or the final result, this goes here. This is similar to the
class MainApp from the demo subcontroller.py
Level
This class does all the hard work. In addition to the initializer (which
is a proper __init__, not start), it needs its
own update and draw methods. This is a subcontroller, and you
should use the demo subcontroller.py (in the provided sample code)
as a template.
The most complex method will be the update and you will certainly
violate the 30-line rule if you do not break it up into helpers. For the
basic game, this method will need to do the following:
• Animate the frog according to player input
• Move all the obstacles in each lane
• Detect any collisions between the frog and an obstacle
• Remove the frog after a death or a successful exit
In our code, each one of these is a separate helper (though the
second one will actually be managed by the Lane class below). You
should think about doing this in your code as well.
The Lanes
There are four types of lanes in the game: road, water, grass, and
the hedge. The hedge is where the exits are placed. The road has
cars (and trucks). The water has logs to jump on. The grass is a
safe zone that gives you a rest.
Because each lane acts differently, you want to make a separate
class for each one. However, they do have a lot in common, so
they will all be subclasses of the class Lane. These classes already
appear in lanes.py with their specifications.
Each lane is essentially a mini-level. The frog needs to get
succesfully past it. That is why each lane will be its own
subcontroller, controlling its own lane. As a subcontroller, each lane
should have its own __init__, update, and draw methods. However,
most of these will go in Lane and the other classes will safely inherit
them. When the Level class needs to update the lanes, all it will do
is loop through the lanes and call the update method for each one.
The Models
The models just keep track of data. Most of the time, models just
have attributes, with getters and setters. Think Image from the
previous assignment. However, sometimes models have additional
methods that perform computation on the data, like swapPixel.
The models in this assignment are the game objects on screen: the
frog and any obstacles. Most of the time, you do not need a new
class for a module, as the class GImage does everything that you
needs for the cars, logs, and exits. However, the frog is very special
and so the frog will need a custom class. We have written the
specification of this for you.
If you want to add any additional features to your game, you may
need to add some new models here. For example, turtles that will
occasionally dive under water have to be animated in the same
way that the frog does. So they will need their own classes for
exactly the same reason. You should only add new classes for the
additional features.
Suggested Micro-Deadlines
You should implement the application in stages, as described in
these instructions. Do not try to get everything working all at once.
Make sure that each stage is working before moving on to the next
stage.
Set up a schedule.
Overview of Froggit
The layout of a Froggit game depends on the level file you are
using. We have included many different level files in
the JSON directory. The files easy1.json and easy2.json are easy games
to help you test your game, while roadsonly.json and complete.json are
a little more challenging. The level that you use is defined by the
variable DEFAULT_LEVEL in const.py. You can also change the level at
any time by specifying it when you run the game. For example, if
you type
python froggit roadsonly.json
it will play the game with the level roadsonly.json as the DEFAULT_LEVEL.
Below is the set-up for the complete.json to test out your game in the
end.
The Starting Position
All of the levels are arranged as a grid, where each grid square
is GRID_SIZE pixels on each side. The lanes are one grid unit in
height, and several grid units in width. Each exit is one grid unit
wide. Cars or logs can take up multiple grid squares, depending on
the specific obstacle. This grid is not explicitly visible, but it
becomes obvious once you play the game for a bit.
The Invisible Grid
Once the game begins, the cars and logs move across the screen.
They go in different directions, specified by the 'speed' key in the
JSON dictionary. Once they go off the screen, they wrap back
around to the other side. How fast they wrap around is defined by
the 'offscreen' key in the JSON dictionary.
The frog has to safely make it to (i.e. touch) one of the exits, which
is a lily pad at the final hedge. When that happens, the game puts a
bluish frog on the lily pad and restarts another frog at the starting
point, as show below. Exits may not be reused. The second frog
cannot use the exit that currently has a frog on it.
At the First Exit
If a frog dies, the game pauses and deducts a life from the player.
These lives are shown by the frog heads in the top right corner. In
the picture below, the game is paused after losing the first life.
Down One Life
There are two ways for the frog to die. The frog dies if it is hit by (or
even touches) a car. The frog also dies if touches the water. Why
this is a frog that cannot swim is a mystery from the early days of
video games. To survive on the water, the frog must hop on a log.
But the logs carry the frog with it when they move, making the
game a little more challenging.
Once you understand those rules, the game is simple. You win by
filling up every lily pad at the hedge. You lose if you run out of lives
before this happens. In the video below, we show both outcomes
on the level easy2.json.
Game State
One of the challenges with making an application like this is
keeping track of the game state. In the description above, we can
identity several distinct phases of the game:
• Before the game starts, but no level has been selected
• When the level has been loaded, but the frog and obstacles
are not moving
• While the game is ongoing, and the obstacles are moving
onscreen
• While the game is paused (e.g. to show a message)
• While the game is creating a new frog to replace the old one
• After the game is over
Keeping these phases straight is an important part of implementing
the game. You need this information to
implement update in Froggit correctly. For example, whenever the
game is ongoing, the method update should instruct the Level object
to move the frog. However, if the game has just started, there is
no Level object yet, and the method update should create one.
For your convenience, we have provided you with constants for six
states:
• STATE_INACTIVE, before a level has started
• STATE_LOADING, when it is time to load the level file
• STATE_ACTIVE, when the game is ongoing and the obstacles are
moving
• STATE_PAUSED, when the game is paused to display a message
• STATE_CONTINUE, when the player is waiting for a new frog
• STATE_COMPLETE, when the game is over
All of these constants are available in consts.py. The current
application state should be stored in a hidden
attribute _state inside Froggit. You are free to add more states if you
add extensions. However, your basic game should stick to these
six states.
The rules for changing between these six states are outlined in the
specification of method update in Froggit. You should read that in its
entirety. However, we will cover these rules in the instructions
below as well.
Level JSONs
All of the information about a game level is stored in a JSON file.
You should remember what a JSON string is from the very first
assignment. A JSON file is just a file that stores a very large JSON
string. These JSON files are a lightweight way to store information
as nested dictionaries, which is the standard way of sending
complex data across the Internet.
The tricky part about JSONs is coverting the string (or file) into the
Python dictionary. Fortunately, we have provided a tool that makes
this part easy. The GameApp class includes a method
called load_json which your Froggit class will inherit. Simply specify
the name of the JSON file. As long as that file is stored in
the JSON directory, this method will load this file and convert to ta
Python dictionary for you.
This means that working with level files is really all about working
with a complex nested dictionary. To understand how these
dictionaries work, look at the file easy2.json. This includes all of the
features that you will need to implement for the basic game. The
top-level dictionary has five keys:
• 'version' : A float representing the level version.
This version is only relevant if you define your own levels. It is
discussed in the section on additional features.
• 'size' : A list of two integers for the width and height.
The size represents the size of your game in grid squares, not
pixels. You multiply these by GRID_SIZE to get the size of the game.
• 'start' : A list of two integers for an x and y position.
The start value is the starting grid square for the frog (grid squares
start at 0 like any list). You multiply its two integers by GRID_SIZE to
get the starting pixel position for the frog.
• 'offscreen' : An integer to support “movement wrap”.
Objects need to wrap back to the beginning once they go off
screen. But you do not want to do that immediately, as the images
will snap and flicker. You want them to go completely offscreen
before you wrap them. This value is how far (in grid squares) that
any image must be offscreen before it is time to wrap it back
around. This is discussed in the activity to move the obstacles.
• 'lanes' : A list of all the lanes in the level.
The list of lanes should have the same number of elements as the
(grid) height of the level. The are ordered bottom up. The first lane
in the list is the bottom row, and the last lane in the list is the top
one.
The real nesting happens in these lanes. Each lane is its own
dictionary, with up to three keys. These are as follows:
• 'type' : The lane type.
The lane type is a string, and is one
of 'grass’, 'road', 'water' and 'hedge'. Unless you add new types of
lanes to the game, there are no other possibilities.
• 'speed' : A float indicating how fast obstacles move in this
lane.
All obstacles in a lane move at the same pace. This is the number
of pixels they move per second. Movement is left-to-right. If the
speed is negative, then the movement is right-to-left.
• 'objects' : The list of obstacles in this lane.
The exact obstacles vary by lane type. For a road, these are the
cars. For a water lane, these are the logs. For the hedge, these are
the exits (which technically count as obstacles).
Not all lanes have all three keys. Grass has no obstacles. The
hedge has exits, but they do not move and so they have no speed.
However, every lane is guaranteed to have a 'type', and it is
guaranteed to be one of those four values.
Finally, there are the obstacles in the list for 'objects'. All obstacles
have two keys:
• 'type' : A string representing the obstacle type.
For this assignment, it is the image file (minus the '.png’ suffix) for
this obstacle. So type 'log2' corresponds to the image file 'log2.png'
• 'position' : The float for the obstacle position.
Technically, the position represents a grid square, so you multiply it
by GRID_SIZE to get the position of the obstacle. However, obstacles
are different from the frog in the fact that they can actually be
between two grid squares (which is why this is a float).
If you understand all of these features, then you will have no
problem completing this assignment.
JSON Assumptions
One of the preconditions for this assignment is that level files are
properly formatted. That is, they are proper JSONs and they do not
have any important keys missing. As we said above, it is okay for
some keys to be missing, like 'speed' or 'objects'. But other keys
like 'size' and 'start' absolutely have to be there.
It is not your responsibility to enforce that the JSON files are in
the correct format. That is the responsibility of the level designer,
who is typically a different person on the team than a programmer.
However if you add any extensions to the game, then you will
likely be designing your own level files. And if you make mistakes in
your level files, you may cause your program to crash. Again, this is
a problem with the level file and not the game itself.
Task 1: Setup
We have divided these instructions into three parts. This task
involves setting up the game. That includes loading a level file and
using it to draw objects on the screen. While you will be able to
move the frog when you are done, nothing else will work. No
obstacles will move and you cannot win or lose the game.
We have made all of the instructions in this section very explicit. As
we move on to later tasks, the instructions will become a little more
vague.
Review the Constants
The very first thing that you should do is read the file consts.py. If
you ever need a value like the size of the grid, the initial size of the
game window, the frog movement speed, or so on, this is where
you go. When writing code, you should always use the constants,
not raw numbers (or “magic numbers,” as we call them). Magic
numbers make your code hard to debug, and if you make a change
(e.g. to make the grid size larger), you have no idea about all of the
locations in your code that need to be changed.
With that said, you are welcome to change any of these numbers if
you wish. You are also encouraged to add more constants if you
think of other numeric values that you need. Anytime that you find
yourself putting a number in your code, ask yourself whether or not
it would make sense as a constant.
Create a Welcome Screen
We start with a simple warm-up to get you used to defining state
and drawing graphics elements. When the player starts the
application, they should be greeted by a welcome screen. Your
initial welcome screen should start with two lines of text.
Because the welcome message is before any game has started, it
belongs in the Froggit class, not the Level class. You are already
seeing how we separate what goes where.
The welcomes will look something like the one above. It should tell
the player the name of the game, and tell the player to “Press ‘S’ to
Start”. You can change the wording here if you want. It could say
something else, as long as it is clear that the user should press a
key on the keyboard to continue. However, we recommend against
allowing the user to press any key, since in later steps that will
make it easy for the user to accidentally miss an important
message (or jump immediately in front of a truck).
To create a text message, you need to create a GLabel and store in
it an attribute. If you read the class invariant for Froggit, you will see
two attributes named _title and _text. The title attribute is the logo
of the game. The text attribute is for any messages to display to
the player. The text is typically smaller, like a footnote. While we will
never show the logo after the initial state STATE_INACTIVE, we will use
the text attribute for messages throughout the game.
Since the welcome message should appear as soon as you start
the game, it should be created in the method start, the first
important method of the class Froggit. When creating your
message, you will want to set things like the font size and position
of the text. If you are unsure of how to do this, look at the
class MainApp from the demo subcontroller.py in the provided sample
code.
As you can see from the documentation for GLabel and GObject,
graphics objects have a lot of attributes to specify things such as
position, size, color, font style, and so on. You should experiment
with these attributes to get the welcome screen that you want.
The first thing to understand is the positioning. In Kivy, the screen
origin (0,0) is at the bottom-left corner, and not the center like it
was for the Turtle assignment. If you want to find the center of the
window (our version centers these on the screen), the Froggit object
has attributes width and height that store the size of the window. In
addition, for the label object, the attributes x and y store
the center of the label. So you can center the label horizontally by
assigning x to width/2.
When placing label objects, you do not always want to center
them. Sometimes you would like the label to be flush againt the
edge of the window. For that reason we have attributes
like left, right, top, and bottom. These are alternate attributes
for x and y. They move the label in much the same way, but give
you a little more control over the positioning.
One you understand how to position the label, it is time to think
about your font choice and style. If you want to look exactlty like
the picture above, we have some constants to help you in const.py.
The font is ALLOY_FONT, which is a reference to AlloyInk). The title has
size ALLOY_LARGE while the message has ALLOY_MEDIUM. However, you are
not constrained to these choices. You may chose a different font or
font size if you wish, so long as it fits on the screen.
Drawing the Welcome Message
Simply adding this code to start is not enough. If you were to run
the application right now, all you would see is a blank white
window. You have to tell Python what to draw. To do this, simply
add the lines
self._title.draw(self.view)
self._text.draw(self.view)
to the method draw in Froggit. The (non-hidden) attribute view is a
reference to the window (much like the Window object in Assignment
4).Hence this method call instructs Python to draw this text label in
the window. Now run the application and check if you see your
welcome message appears.
Initializing the Game State
The other thing that you have to do in the beginning is initialize the
game state. The attribute _state (included in the class specification)
should start out as STATE_INACTIVE. That way we know that the game
is not ongoing, and the program should (not yet) be attempting to
animate anything on the screen. In addition, the other attributes
listed (particularly _level) should be None, since we have not done
anything yet.
The _state attribute is an important part of many of the invariants in
this game. In particular, we want your new attribute for the
welcome message to have the following invariant:
• If the state is STATE_INACTIVE, then there is a welcome message
with title and text.
• If the state is not STATE_INACTIVE, the _title attribute is None.
• If the state is STATE_ACTIVE, the _text attribute is None.
Does your start() method satisfy this invariant? Note the difference
between the last two invariants. That will become important later.
Dismissing the Welcome Screen
The welcome screen should not show up forever. The player should
be able to dismiss the welcome screen (and start a new game)
when he or she presses the S key. To respond to keyboard events,
you will need the attribute input, which is an instance
of GInput.This class has several methods for identifying what keys
are currently pressed.
When using the attribute input, remember the issues that we
discussed in class. The method update(dt) is called every 16
milliseconds. If you hold a key down, then you see a lot of key
presses. You just want the first press! That means you need some
way to determine whether or not the key was pressed this
animation frame and not in the previous one. See the state.py demo
from the sample code for some ideas on how to do this. This may
require you to add a new attribute to Froggit.
If you detect a key press, then you should change the
state STATE_INACTIVE to STATE_LOADING. This will load a level file and start
a new game. You are not ready to actually write the code to start
the game, but switching states is an important first activity.
Invariants must be satisfied at the end of every method, so you
need to assign None to both _title and _text now. This will require a
simple change to method draw() to keep it from crashing (you
cannot draw None). Once you have done that, run the application.
Does the message disappear when you press a key?
Documenting your New Attributes
When working on the steps above, you may have needed to add
new attributes beyond the ones that we have provided. Whenever
you a new attribute, you must add it and its corresponding
invariant as a comment after the class specification (these
comments are the class invariant). Add it just after the comment
stating ADD MORE ATTRIBUTES, to make it easier for the graders (and you)
to find them. We will deduct style points for instance attributes that
are not listed in the class invariant
Pacing Yourself
This first part of the assignment looks relatively straightforward, but
it gets you used to having to deal with controller state. Try to finish
this part by Wednesday, December 2, which is right after you have
had your first lab. This will give you time to familiarize yourself with
the online documentation, and make sure that you understand
how everything fits together.
Load the Level File
The next activity is a little more complicated than the welcome
screen, but once you complete it, you can be confident that you
have a good idea how everything fits together. The
state STATE_LOADING is only supposed to last one animation frame.
During that frame you should do the following:
• Load the DEFAULT_LEVEL into a dictionary
• Resize the window to match the level
• Create a new Level object
• Assign that Level object to the attribute _level
• Display the contents of the Level object
• Switch the state to STATE_ACTIVE
You do not need to worry about the details of STATE_ACTIVE for now.
However it is very important that you only create a
new Level object if the state is STATE_LOADING. If you continue to
create a Level object in STATE_ACTIVE, that will cause many problems
down the line (which you will not notice until much later).
Loading a JSON File
Loading a JSON file is easier than you think. There is a method
(technically it is a class method) in GameApp called load_json,
and Froggit inherits this method. Simply call this method on the
constant DEFAULT_LEVEL. The method will return a dictionary and you
are good to go. So from this point on, you will treat the level
information like a dictionary. Use the key 'size' to get the width and
height (in grid squares) for the level.
By default, the starting level is easy1.json. You can go into consts.py to
change this if you wish. Alternatively, you can test other levels by
typing
python froggit levelname
So you can load the 'complete.json' level simply by typing
python froggit complete.json
Resizing the Window
Different level files are different sizes. When you load a level, you
want to resize the window to match the level. The window width
should be the level width times GRID_SIZE. The window height should
be one grid size higher than the level height. That is because we
need an extra grid square to display the remaining lives.
To resize the window, remember that the Froggit object
has mutable attributes width and height. Just assign the value to
those. If you do it right, the levels 'easy1.json' and 'easy2.json' should
make the window slightly smaller, while 'complete.json' will not resize
the window at all.
Creating a Level Object
We have started the definition of the Level class for you in levels.py.
However, it does not do much, because we have not defined the
initializer. Furthermore, this means that the constructor does not
take any arguments. However, you want the Level constructor to
take a single argument: the JSON dictionary containing all of the
information about the level.
Inside of your initializer, your are going to take the 'lanes' list from
this dictionary and create the lane objects. For right now, each lane
is going to be a single GTile object. A GTile is an image that can be
repeated. To draw grass, road, or water, we take a single image
and repeat several times. For example, the image below shows the
difference between the image 'hedge.png' and 2x3 tiling of that same
image.
Normal Image vs 2x3 Tiling
To define a tiled image, you use the attributes x, y, width, height,
and source to specify how it looks on screen. The first four attributes
are just like GLabel, while source specifies an image file in
the Images folder. As with the label, you can either assign the
attributes after the object is created or assign them in the
constructor using keywords. Keyword arguments work like default
arguments in that you write param = value. See the online
documentation for an example of how to approach this.
The source for a lane tile is just the type plus the suffix '.png', So
the 'grass' type uses the image 'grass.png', the 'road' type uses the
image 'road.png' and so on. The height should be GRID_SIZE, while the
width should be the width of the window. The only hard part is
positioning the tiles.
The lanes are placed starting at the bottom of the screen and work
towards the top. There will be one blank line at the top, because
you set the height of the window to be GRID_SIZE higher than all of
the lanes. To place the lanes you might find it easer to use the
attributes left and bottom. These are alternate attributes
to x and y when you do not necessarily want to work with the
center of an object. For example, the first lanes
has left and bottom both 0, while the second lane has its bottom
at GRID_SIZE.
This suggests that you can create all of the lanes using a simple
loop in the initializer, adding them to the attribute _lanes as you go.
Drawing the Level Object
Once again, creating an GTile object is not enough to draw it on the
screen. But drawing the lanes is a bit more complicated than
drawing the welcome message. The lanes are (hidden) attributes
in Level. While the code
for lane in self._level._lanes:
lane.draw(self.view)
works (and you should try it out), it is not allowed. We will take off
major style points if a class ever accesses the hidden
attributes of an object of another class.
This is the purpose of adding a draw method to class Level.
The draw method in Froggit should call the draw method in Level, and
this in turn should call the the draw method for each lane (defined
in GObject).
However, only Froggit has access to the attribute view, which is
necessary for drawing. The class Level cannot directly access any
attributes in Froggit. If a method in Level needs an attribute
from Froggit, then Froggit must provide that attribute as an argument
in the method call. This means that the draw method in Level needs
to have view as a parameter, and the code in Froggit should look like
this.
self._level.draw(self.view)
Notice this is very similar to how we draw GObject objects.
Testing Your Code
While the concept of level files is a lot to wrap your head around,
the advantage is that they make it really easy to test your code. By
running your game on several different levels, you can see if you
did them correctly. Remember, to try out a different level, type
python froggit levelname
Here are what the backgrounds should look like for some of the
provided levels.
Background for easy1.json
Background for easy2.json
Background for roadsonly.json
Background for complete.json
WARNING: These files are designed assuming that you have a
1080p (1920x1080 pixel) monitor for your computer or laptop. If
you have a 720p monitor (1280x720 pixels), you will be able to play
the first two levels, but not the last two. In addition, we have found
that the default settings on the 13 inch MacBook are 1440x900
pixels making complete.png unplayable (though the others are fine).
However, you can solve this problem by going
into Settings > Display and choosing More Space.
Pacing Yourself
This is a longer activity, and we have budgeted up to two days to
work on this. That means you should try to finish this part
by Thursday, December 3,. However, if you take both of the days,
then you should complete the next activity on the same day.
Completing this activity successfully guarantees you will pass
(C-) this assignment.
Create the Frog
Next you need to create the frog. Again, this is to be stored in an
attribute of class Level. That means that you must create it in
the __init__ method of Level and modify your drawing code so that it
appears. The frog should be an object of class Frog, which is
included in models.py.
You will notice that Frog is a subclass of GImage. Like GTile,
a GImage object is used to draw an image to a screen. The difference
is that, if you specify the width and height, the GImage will resize the
image to fit entirely in that size; it will not tile the image. But
otherwise, creating and drawing a GImage is exactly the same as
creating and drawing a GTile object.
Initializing the Frog
Technically, we do not have to define an initializer for the frog. We
inherit the one from GImage already. But we do not want to use that
initializer. The image file for the frog will never change; it is defined
by the constant FROG_IMAGE in const.py. And the width and height will
be the default values. The only thing we want to specify is the frog
position on the grid.
So that means we want to define a custom initializer for
the Frog class. This initializer should have parameters for the start
position (both x and y) and that is it. This initializer then
calls super() to use the initializer for GImage, passing FROG_IMAGE as the
source file.
When positioning the frog, the center of the frog (defined by the
attributes x and y should be the center of the grid square.
Remember to multiply the grid positions by GRID_SIZE before passing
them to the initializer for GImage. The GImage class works with pixels,
not grid squares. But also remember that a grid square (col,row) is
not centered at (col*GRID_SIZE,row*GRID_SIZE). That would be
the bottom left corner of the grid square.
There is one more thing you need to do when you create the frog.
By default, the frog image is facing to the south. You need to set
the angle attribute of the frog to FROG_NORTH to make sure the frog is
facing in the correct direction.
One you have created the custom initializer for Frog, the initializer
in Level just needs to call the constructor for Frog and assign it to
the _frog attribute.
Drawing the Frog
Remember to draw the frog object in the method draw of class Level.
You draw the frog in the same way you drew the lane tiles. You do
not need to define a draw method in class Frog as it is inherited
from GImage. However it is important that you draw the frog last.
Objects are draw bottom to top and you want the frog to be on top
of everything.
Testing Your Code
Again, you can test your code by running each of the provided level
files. For all of the provided levels, the frog should be centered in
the bottom lane. For example, the level easy1.json should look like
this:
Frog for easy1.json
That is because 'easy1.json' has a start position of [5,0]. If instead
the start position where [0,5], it would look like this:
Frog for Swapped easy1.json
Pacing Yourself
This is super short activity. You should be able to complete this
soon after you finish drawing the lanes, which is by Thursday,
December 3. Finishing this on time will give you more time for the
next activity.
Completing this activity successfully will guarantee you a low
C on this assignment.
Add the Obstacles
Now we come to the first tricky point of the assignment. We want
you to create all of the obstacles in the level. This includes cars,
trucks, logs, and even exits. But do this you are going to need
to replace some of the code that you have already written. This is
something that we are going to be doing throughout the
assignment. You will start with simple code first to make sure
everything is working. But then you will go back and replace it with
more complex code.
The code you need to replace are the GTile objects you created in
the initializer for Level. That is because we are going to replace
those objects with a composite object. If you do not know what
composite objects are, these are discussed in lab 22, soon after
you get back. You may go over this lab with your lab instructor if
you need help.
For those who wish to skip the lab, composite object is when we
combine one or more GImage objects (or in this case a GTile object
and some GImage objects). The composite object is constructed just
like a subcontroller. It has an initializer that creates all of the objects
inside of the composite, and its own draw method to draw the
individual objects as well.
Initializing a Lane
The Lane object will serve as our composite object. You will notice
that there are classes for Grass, Road, Water and Hedge. However, you
can safely ignore all of those for now. We will only need those
classes later when we work on the basic game. Right now, the
parent class Lane will do all of our work for us.
Each Lane object will replace a single GTile. That means that
each Lane object should have an attribute _tile for the GTile that it
contains. Ideally, you can just copy the code from Level into the
initializer for Lane. However, that means you need to add some new
parameters to the initializer for the Lane. In addition to having
access to the JSON dictionary, the Lane initializer needs to
know which lane it is, so it can pick the right values out of the
nested dictionary. If you created the tiles with a for-loop, you
essentially need to pass the loop variable as a parameter to the
lane object.
Inside of the Lane initializer, you should also initialize all of the
obstacles, adding them to the list attribute _objs. You do this with a
for-loop just like you did the tiles in a previous activity. However,
this time all of the obstacles are represented by a GImage, not
a GTile. The source file for each obstacle is the same as the type
plus the suffix '.png'. So obstacle 'log2' corresponds to the image
file 'log2.png', and so on.
You do not need to specify the width and height of these images. The
default value is fine. However, you do need to specify their position.
If you look at the JSON files, you will notice that each obstacle has
a 'position' value. This is the grid square for the center of the
obstacle. So the y attribute of the obstacle should be the same as
the y attribute for the tile (the centers should match), but
the x attribute should be determined by the 'position'.
When using the 'position', note that obstacles can fill up more than
one grid square. For example, 'log2' is two grid squares in width.
So to put this obstacle in grid squares 1 and 2, its center will be at
grid 1.5 (so at the border of grid squares 1 and 2). This is shown
below.
Positioning an Obstacle
This means that you cannot get the x pixel by multiplying
the 'position' by the GRID_SIZE. But it is close, and you should be able
to figure out the correct answer now.
Orienting the Obstacles
In addition to placing the obstacles, you need to orient them
correctly. By default, all obstacles are facing the right edge of the
window, because the assumption is that they will move left-toright.
However, that is not always true. Some objects will move
right-to-left, and they will need to face in the opposite direction.
How do we know which is which? It depends on the 'speed' value of
the lane. If the speed is positive (or 0), then the obstacles face the
normal direction. But if the speed is negative, the obstacles face
the opposite direction.
Turning obstacles around is easy. We just rotate them 180 degrees
by senting the angle attribute in a GImage object to 180.
Constructing a Lane
Once you have the initializer defined for the lane, now it is time to
replace the code in Level. In the initializer for level, you want to
replace all of the GTile objects with Lane objects. We also
recommend that you use the right subclass for each type. So
a 'grass' lane should use a Grass object and so on. We know you did
not define any methods in these classes, but they inherit the
initializer from Lane. And breaking your lanes into specific classes
now will make parts of Task 2 much easier.
Drawing a Lane
Drawing a Lane object is similar to drawing Level object. You need to
add a draw method and it needs to take the view as a parameter. You
use this to draw the tile first (since it is at the bottom) and then all
of the obstacles in order.
If you do this correctly, you should not even need to change the
code in the draw method for Level. That code should still work.
Instead of calling the draw method of GTile, it is calling the method
for Lane.
Testing Your Code
Once again, you should try out your code on several different level
files to make sure that it is correct. Here are what the obstacles
should look like (with the frog included) for some of the provided
levels.
Obstacles for easy1.json
Obstacles for easy2.json
Obstacles for roadsonly.json
Obstacles for complete.json
Pacing Yourself
This is the hardest activity in the setup portion. Give yourself two
days to work on this activity, finishing it by Saturday, December 5.
Once you get this down, the rest of the setup will be easier.
Completing this activity successfully will guarantee you a C on
this assignment.
Display the Lives
The challenge of Frogger is that you have a limited number of lives
before the game is over. We want to display those lives to the
player. That is the purpose of the blank lane at the top of the
screen. When you are done with this activity, the top bar should
looke like this:
The Lives Counter
The lives are represented by GImage objects using the source
file FROG_HEAD. However, the FROG_HEAD image is very large. You will
need to scale it to GRID_SIZE width and height to get it to fit. You can
do that by setting the width and height attributes of the GImage object.
You should keep these GImage objects in a list. That makes drawing
them the same as drawing the lanes. And when you want to lose a
life, you will just remove one of the images from the list.
Finally, you should also include a GLabel indicating that these frog
heads represent the lives. If you use ALLOY_FONT, we recommend
using ALLOY_SMALL for the font size. In addition, make the right edge of
the label equal to the left edge of the first head.
Organizing Your Code
All of these attributes should be created in the initializer for Level.
However, you might notice that your initializer is starting to get a
little long. Remember the 30 line rule. If any method gets longer
than 30 lines, you might need to break it up into helpers. This may
now be the time to do that.
If you make helpers, remember to write specifications for all of your
helpers. You should also hide them. If another class does not need
access to them, they should be hidden.
Finally, remember to update the draw method to draw the label and
the frog heads. If you think that your draw method should be broken
up into helpers, you should do that as well.
Pacing Yourself
This is a short activity, though we have give you a day on it. Try to
finish this by Sunday, December 6. That should give you a
breather if you got behind on the previous activity.
Completing this activity successfully will guarantee you a high
C on this assignment.
Move the Frog
The last activity in the setup is to get the frog moving. We will not
worry about the obstacles yet. We just want to concentrate on the
frog. To move the frog, you will need to take into account the
player’s key presses. The frog only moves when the player presses
(or holds down) a key to make it move. By default, we assume that
the player will use the arrow keys to move the frog. However, if you
prefer WASD controls or some other control scheme, that is okay.
Updating the Frog
To see how to control the frog, you should look at the arrow.py demo
from the provided sample code. This example shows how to
check if the arrow keys are pressed, and how to use that to
animate a shape.
To perform the actual movement, you will need to add
an update method to Level. This will complete the methods turning it
into a proper subcontroller, like subcontroller.py in the sample code.
Note that moving the frog requires access to the input attribute.
This is an attribute of Froggit, not Level. The Level class will need
some way to access this.
The frog should move between grid squares. That means if the
player is pressing the up or down arrow, the frog will
move GRID_SIZE up or down. If the player is pressing left or right, the
frog will move GRID_SIZE left or right. As a result it will look like the
frog is slightly “teleporting” about the screen, as shown below:
Note that the frog also turns when it moves. Use the
constants FROG_NORTH, FROG_EAST, FROG_SOUTH and FROG_WEST to point your
frog in the correct direction.
If the player is holding down two keys at the same time, the frog
should not move diagonally. One of the keys should win (or they
should cancel out). Which one you do is up to you. We do not care.
Timing the Movements
If the player holds down an arrow key, we actually want the frog to
keep moving. So in a sense, moving the frog will easier arrow.py. We
do not care whether the player did or did not press a key the
previous frame. As long as the player holds down the arrow keys,
the frog will move.
However, part of the challenge of the Frogger is that the frog
moves slowly. If the frog moves a full GRID_SIZE every animation
frame, the frog will rocket off the screen immediately.
The solution to this it to introduce a cooldown. Once the frog
moves, you must wait FROG_SPEED seconds to move the frog again.
How do you keep track of time? That is the point of the dt attribute
in the update method of Froggit. That measures how many seconds
have passed. So, when the frog moves, set a cooldown attribute
to FROG_SPEED. Afterwards, subtract dt from the cooldown. Once this
reaches 0 or less, the frog can move again.
Restricting Movement
You should ensure that the frog stays completely on the board
even if the player continues to hold down a key. If you do not do
this, the frog is going to be completely lost once it goes off screen.
How do you do this? Look at where the frog will end up before you
move it. If it is going to go offscreen, then do not move
it. However if that movement required the frog to turn, you should
still turn the frog in place, even if it cannot move forward.
When we say “offscreen”, we mean the top bar with the lives
counter in it as well. As you can see in the video above, our frog
stops when it reaches the final hedge. However, the frog can still
move freely about the hedge. That will not be the case in the game;
the hedge will block the frog unless the frog is stepping on a lily
pad. But we will not worry about that until a later activity.
Testing Your Code
Testing this part of the code is easy. Run the game, and try to
move your frog. Does the frog move? Does it stay on the screen?
Then you have succeeded.
This activity is actually pretty straightforward, provide you
understand the arrows.py demo in the provided sample code. Most
people can complete this part of the assignment in less than two
hours. However, some people will find that they cannot get the frog
to move, even after properly adapting the code from arrows.py.
One of the key things is to make sure that your cooldown is
correct. Keep one of the arrow keys held down. The frog should
not move smoothly, but instead should only move
every FROG_SPEED seconds. It is possible to adjust this speed from the
command line if you wish. Try running
python froggit easy1.json 1
The last argument is a number which replaces FROG_SPEED. In this
case, the frog would move once a second, which is much slower
than the default 4 times a second.
Debugging Your Code
If your frog refuses to move, add a watch statement to
the draw method of Level. Print out the id (the folder name) of the
frog. Run the game and look for this print statement. What you see
will (hopefully) identify your bug.
The print statement never appears: In this case, you have forgot
to call the update method for Level from Froggit. Make
sure Froggit has this line of code in its update:
self._level.update(self.input)
The frog id keeps changing: In this case, you are accidentally
reseting the Frog object each frame. Go back and look at the
instructions for loading the level file to see how to stop this.
The frog id is constant: If this is true and the frog still will not
move, we have no idea what your problem is. It is something
unique. Please see a consultant immediately. They will not look at
your code, but they will help you debug it better. In our experience,
when students claim the frog id is not changing, they are wrong.
Pacing Yourself
We highly recommend that you complete this by Tuesday,
December 8, ending the first week of work. This will get you to the
first grade border.
Completing this activity successfully will guarantee you a C+
on this assignment.
Task 2: The Basic Game
When you are done with this task, you will have a complete and
playable game. It will not be fancy, but it will be playable. That will
be enough to put you at the B+/A- border.
Because we expect more of B students than we do of C students,
these instructions will not be as detailed as they were for the
previous task. At this point, we will have assumed that you have
read all the documentation and are familiar with how
the game2d objects work.
Detect the Exits
In the previous activity, the frog could walk all along the hedge.
This is not what we want. The frog should be able to enter an exit
(one of the lily pads). Any other part of the hedge will block the frog
from moving, just as if it were going offscreen.
To do this, we need to be able to do two things
• Detect if the frog is trying to walk into a hedge
• Detect if the frog is trying to walk into an exit (or an opening)
To do this, we will leverage two different methods inherited
from GObject: collides and contains. They are both very similar, but
they do have some important differences.
Detecting Movement into a Hedge
The collides method is used to determine when two game objects
(like say a frog and a background tile) are overlapping. To
determine if the frog is in the hedge, simply call the
method collides from either the frog or the hedge tile (it does not
matter which). If the result is True, then the movement is (potentially)
blocked. Otherwise, the frog is not trying to move into the hedge
and there is no problem.
Because the tile object is a (hidden) attribute of Lane and the frog
object is a (hidden) attribute of Level, we need some way to
compare these two together. This will require additional methods in
the Lane class. You either need a getter to access the tile, or a
collision method that allows the Level to pass the frog as a
parameter. We do not care which one you chose.
When you write this method, be prepared for there to be multiple
hedges. We have added an image 'open.png' which represents an
opening in the hedge that is not a exit. This allows the player to
walk through the hedge without reaching an exit. This is for
advanced levels like 'multihedge.json'. While supporting this file is
not necessary for this activity, we will be including it when we
grade this task at the end.
Detecting Movement into a Exit
Movement into a hedge is only allowed if it is into an exit (or an
opening). As the “obstacles” of a hedge are all either exits or
openings, this means that we just have to check whether the frog
collides with an obstacle in the hedge. If so, the frog can move into
the hedge.
However, this time we do not want you to use the collides method.
For reasons that will become clear when you give the frog a log
ride, we want to ensure that most of the frog overlaps the exit. So
that is the purpose of the contains method. The contains method
checks if a point (represented as a tuple (x,y)) is located inside of a
game object. We only want to allow movement into the hedge if
the center of the frog will be contained in the exit.
To do this, you loop though the exits and opening (the obstacles)
and see if the center of the frog is contained in one of them. If so,
the movement is allowed. Adding this functionality will require more
methods. But this time we do not want you to add these methods
to Lane. This is functionality that is specific to a hedge. So these
methods (even if they are just getters to access all of the exits)
belong in the Hedge class. This is where we are going to start to use
our subclasses, as each of the lanes will behave differently from
each other.
Testing Your Code
Once again, test your code by trying out some of the levels. You
should be blocked trying to walk into a hedge unless you are
walking into an exit or opening.
You should also try out the level 'multihedge.json' and see if you can
squeeze through the first hedge to make it to the second hedge. It
is okay if you walk through exits like they were an opening. You will
solve that problem in a later activity.
Pacing Yourself
You should be able to finish this activity in a single day. If you are
on schedule, this would be by Wednesday, December 9. This
activity is not that much more difficult than the normal restrictions
on frog movement. The only challenge is spreading out your code
among the individual classes.
Completing this activity successfully will guarantee you a B- on
this assignment.
Move the Obstacles
Up until now, the obstacles have just been sitting there. It is time to
get them moving. Movement is determined by the 'speed' value for
the lane. This is the number of pixels that each obstacle should
move per second. All obstacles in a lane move at the same rate. If
the speed is positive, they move left-to-right (so you add the speed
to the position). If the speed is negative, they move right-to-left.
Implementing Basic Movement
To implement the basic movement, you will need to add an update
method to Lane, also turning it into a proper subcontroller.
This update method should move all of the obstacles in the lane, and
it should be called from the update method in Level.
Technically, you should only move obstacles in road and water
lanes. However, hedges never have a speed (the exits do not move
unless you are making than an extension). And the grass has no
obstacles (again, unless you are adding an extension like snakes).
So that is why it is safe to add this code to the Lane class. Keep in
mind that this movement will require some additional attributes
(such as the lane speed).
When you move the obstacles, remember that the speed is the
number of pixels per second. To get the number of pixels to move
any given animation frame, you should multiply that value by dt, the
time since the last animation frame.
Implementing Wrap Around
Eventually the obstacles are going to go offscreen. When that
happens, we want to wrap them back around to the other side. So
objects going offscreen to the left should come back around on the
right, and objects going offscreen to the right should come back
around to the left.
Naively, the way to do this is to look at the x position of the
obstacle. If it is less than 0, set it the width of the window. If it is
greater than the width, set it to 0. However this solution causes a
major problem: “snapping”. The x attribute is the center of the
object. So we will see have of the object immediately disappear on
one side, and the other half teleport in on the other side. We want
the movement to be smooth.
We could try to solve the snapping problem by using
attributes left and right instead of x. If the left attribute is fully off
the right of the screen, we set the right attribute to 0. And similarly
if the right attribute is less than 0, we set the right attribute to the
width. This gets rid of snapping and the movement looks smooth.
However, it completely screws up spacing. Different obstacles have
different lengths (look at level 'easy2.py'). Over time, this will alter
the spaces in-between the obstacles changing the layout of the
level. In some cases, it could cause the obstacles to start to
overlap.
We want a solution that eliminates snapping but also preserves
spacing. The solution is to have an offscreen buffer. This is a little
bit of a distance that all objects are allowed to go offscreen. If the
buffer is 4, then obstacles can have an x as low as -4*GRID_SIZE and
as high as width+4*GRID_SIZE. When the x attribute crosses this
threshold, then the object should wrap around to the other
offscreen edge. Hence an object will traverse two offscreen buffers
before it comes back on screen.
The buffer should be large enough to support the largest obstacle
in the level. That is why it is defined inside of the level file. It is not
your responsibility to ensure that the buffer is large enough. That is
the responsibility of the level designer.
An Offscreen Buffer of Size 2
As the illustration above shows, the the object should not snap to -
buffer*GRID_SIZE or width+buffer*GRID_SIZE when it wraps around. If you
do this, you will still start to lose spacing over time because the
obstacles do not align exactly with the grid squares. Instead, when
the center of the obstacle (marked by the blue dot) crosses the
buffer, we determine the distance d that it went over. When we
teleport the image to the other side, we shift it forward by the
amount d.
This all sounds harder than it is. You can accomplish all of this with
a simple if-statement.
Testing Your Code
Once again, you can test your code on any level file. However, we
highly recommend that you test it on the level 'complete.json'. If it is
working properly, then it should look like the video below.
Let the game run for a long time. If you are losing spacing, then
you will start to see it in this level, because there are so many
obstacles.
Pacing Yourself
The code is getting harder, but we still think you can finish it in a
day. If you have made it this far then you understand more and
more of how the code works, and can write code faster. Therefore,
you should try to finish this activity by Thursday, December 10.
Completing this activity successfully will guarantee you a low
B on this assignment.
Squash the Frog
Now we have everything moving, but the game is not very
interesting. That is because nothing really interacts with anything.
Cars do not squash the frog. Logs do not take the frog for a ride.
Only hedges really do anything right now, but it does not feel like a
game.
In this activity, you will introduce the first challenge in your game:
cars. If a frog collides with a car, then the frog should die. Before
we talk about how to kill the frog, we first need to think about how
to detect a frog death. In many ways this is exactly the same
problem as detecting the exits. We need to add special methods to
the Road class that allow us to determine if a frog has collided with a
car. Look at how you solved that problem and come up with
something similar here.
Killing the Frog
What does it mean to kill the frog? Two things. First of all, the frog
should disappear from the screen. There are many ways to do that.
One is simply to set the _frog attribute to None, and make sure that
the Level does not draw the frog when it is None (just like
the Froggit class handles the welcome message). Another approach
is to add a visible attribute to the frog. When this attribute is False,
the frog is not drawn. This would require you to override
the draw method in Frog, instead of simply inheriting the one
from GImage. Choose whichever approach you are most comfortable
with.
The second thing about killing the frog is that you should pause the
game immediately. That is, the Froggit object should switch its state
to STATE_PAUSED (remember states?) and it should no longer update
the Level object until it is active again. That means you will need
some way for the Froggit app to know whether or not to pause the
game. You could add a getter to Level for Froggit to get this
information. Or you could turn the update method in Level from a
procedure into a fruitful method, and use the return value to
indicate whether or not to pause. Again, we do not care which
approach you take.
Pausing the Game
Pausing the game is easy. Simply do not call update in Level. Again,
you can see exactly this idea if you examine subcontroller.py in
the sample code.
However, there is one more thing to do. We want you to display a
message that the game is paused to the player, just like we have
done below.
Paused Game
Notice that we are displaying a message to the player. We can do
this with _text attribute in Froggit, provided that we draw it last.
There is not need to add a label to Level for this, especially
since Froggit is in charge of pausing.
In the example above we made a few stylistic choices that you
might want to adopt, but you are not required to do so. First of all,
we made the message neatly fit in a center lane of the level (which
is easy if you use ALLOY_SMALL). Furthermore, we set the background
of the GLabel to a solid cover, covering up the obstacles
underneath. We did all of this to make the label more readable. In
the end, this is all that matters. You can do whatever you want with
your paused message so long as it is still readable.
Some of the more observant of you might realize that the message
can get really tight as the levels get small. The level 'easy1.json' is
pretty narrow, but what if we have a level that is just three grid
squares wide? Actually, this is a precondition that we are going to
add to the game. No level will be less than 10 grid squares wide
and less than 8 grid squares high. So as long as your message fits
in that space, you are fine.
Resuming the Game
The player should be able to start playing again by pressing a key.
Our solution uses the ‘C’ key, but it can be anything you want so
long as the message you displayed makes it clear. Once again, we
recommend that you do not allow the player to press any key, as a
player holding down the up arrow will immediately move forward
and step in front of a truck.
This step is almost the same as STATE_INACTIVE. You detect a key
press and then switch the state. But this time you do not want to
switch to STATE_LOADING; we do not want to load a new level. Instead,
go to STATE_CONTINUE.
The purpose of STATE_CONTINUE is to reset the level. That means
making the frog visible again and putting the frog back in its start
location. If you forgot where the start location was, you might need
an attribute somewhere to remember it. Like STATE_LOADING, this is a
state that lasts exactly one animation frame. When it is done, it
should immediately switch to STATE_ACTIVE and the game should
continue as normal.
Testing Your Code
It is time to start killing frogs! Load up a level and step in front of
cars. You do not lose any lives yet, so it is harmless. Make sure
that you die when you touch a car and live when you avoid them.
Keep in mind that images have a little bit transparency around the
edges, but the collision code in GObject simply compares the size of
the images (which are rectangles). So it is possible that your frog
just barely escapes the car but the game still registers it as a touch.
This is a problem you will solve when you tighten the hitboxes.
Pacing Yourself
You should try to finish this by Friday, December 11. A lot what
you need to do here is similar to what you had to do for the exits.
The only new and tricky part is pausing and unpausing the game.
Completing this activity successfully will guarantee you a lowto-mid
B on this assignment.
Lose the Game
Now that you can kill your frog, you can lose the game. Every time
that the frog dies, you should remove one of the frog heads from
the top of the screen. Since this is just a list of GImage objects, this
should be easy. The problem is what happens when there are no
lives left.
Normally when the frog dies, Froggit is supposed to switch
to STATE_PAUSED. However if Froggit detects that there are no lives left
(this will probably require a new method in Level) then it should
instead switch to STATE_COMPLETE and display a very different message
to the player
Game Lost
For the basic game STATE_COMPLETE is the final state. There is no other
state to switch to. The player is expected to close the window and
relaunch the game if they want to play again. If you want to allow
the player to play again, that is an extension that you can add.
Testing Your Code
Once again, it is time to kill your frog. But when you run out of lives
the game should end. Since you cannot win yet, the only thing you
can do is die. This says something about the inevitability of our
mortality.
Pacing Yourself
This activity should be really quick. If you know how to pause the
game, tracking lives and marking a loss is not that much different.
However, we budgeting a full day for this, up to Saturday,
December 12. That is to allow you to catch up you got behind on
previous activities.
Completing this activity successfully will guarantee you a mid
B on this assignment.
Reach the Exits
You are already detecting the exits in the game. But that is not the
same as reaching safety. When a frog touches an exit (as
determined by the contains method), you need to put the frog safely
on the lily pad. That means putting the image FROG_SAFE on top of the
lily pad. You should also make the normal frog invisible and pause
the game, just like when the frog got hit by a car. But this time, the
player should not lose a life.
As when the frog dies, the player will continue once they press the
correct key. The frog will be reset to the starting position to begin
again.
Blocking the Exits
For the most part, this activity is not that much different that
getting hit by car. It is just that the frog does not lose a life.
However, there is one important additional detail. The frog can only
use an exit once. Once a lily pad has been taken, it cannot be used
again in the future. If a frog tries to step into an occupied lily pad,
the frog should be blocked, just like the frog is blocked by a normal
hedge square.
This is going to require a lot more attributes in the Hedge class. In
fact, you are going to want to add some attributes just do draw
the FROG_SAFE images on top of the lily pads. But you also might want
something else (like a list or dictionary) to keep track of which exits
are occupied and which are not.
More attributes means you need to define an initializer in
the Hedge class. It should add the new attributes, but use super() to
make sure that you inherit all of the original attributes from Lane. You
will also need to modify the methods you wrote when you
first detected the exits to account for these changes. Of all
the Lane subclasses, Hedge will prove to be one of the most complex.
Testing Your Code
You are almost ready to play a complete game. Try out the default
game 'easy1.json'. See if you can fill up all of the lily pads without
losing too many lives. If you want more of a challenge, try
‘roadsonly.json' instead.
Pacing Yourself
You should try to finish this by Sunday, December 13, which is
one day ahead of the last activity. We starting to move very fast
now. But students who reach this far should have been able to
finish the other parts a little early and hopefully have some time to
spare in their schedule.
Completing this activity successfully will guarantee you a midto-high
B on this assignment.
Win the Game
Winning the game is lot like losing the game. When you run out of
exits, the game pauses, Froggit switches to STATE_COMPLETE and
displays a congratulatory message like the one shown below.
Game Won
The trick is determining whether the game is won or not.
Class Hedge should give you some method to determine if all exits
are occupied. But remember that it is possible for there to be more
than one hedge, so you need to check this against all hedges.
Testing Your Code
You are now able to successfully play any level that does not
include water. Try out 'easy1.json’ first, as that is very easy level to
win. Try out 'roadsonly.json' for more of a challenge.
Finally, try out 'multihedge.json' to make sure that you work correctly
when you have more than one hedge. Because occupied lily pads
prevent the frog from going through the hedge, this level should
work correctly so long as you did not think that the open square
was an exit.
Pacing Yourself
This activity is very short and should be completed by Sunday,
December 13, the same day as the last activity.
Completing this activity successfully will guarantee you a high
B on this assignment.
Splash the Frog
The game is complete as far as roads are concerned. But the
standard game of frogger also has water. To try out the water
levels, we suggest that you switch DEFAULT_LEVEL to easy2.json. You will
be able to win and lose this level, but the water portions will be
boring. The frog will walk on water and not interact with the logs.
Taking a Log Ride
The frog should go for a ride if it mostly collides with a frog. You
should use the same rule that you use for exits. That is, use
the contains method on a log to check if the center of the frog is
inside the log. When that happens, we say that the frog is “on top
of the log”.
As long as the frog is on top of the log, the log should push it. That
means however much much the log changes its x attribute each
frame, the frog x attribute should change the same amount. When
the frog moves again, this will stop (unless jumps on top of a log).
Right now, movement is discrete. The frog jumps between lanes,
but is always either fully inside of or fully outside of a lane. So that
makes the code somewhat simple. Check which lane the frog is in.
If it is a water lane, find the log underneath the frog (if any). Then
move the frog by that amount.
Notice that this is going to screw up the grid movement. As a frog
gets pushed, it becomes like an obstacle and is no longer
guaranteed to be in a grid square. But because we are using
the contains method to reach the exit, everything will be fine.
Drowning the Frog
The Frogger frog cannot swim. Maybe its mother never taught it
how. What this means is that if the frog enters a water lane but it is
not on top of any log, it dies immediately. You should try it exactly
as if the frog were hit by a car. The game pauses and the player
loses a life. If this is the last life, the player loses the game.
In addition, the frog cannot go offscreen. While you previously
wrote code to prevent this from happening, that was only for the
arrow keys. There is nothing to keep a log from dragging a frog
offscreen. If the center of the frog (defined by the x and y attributes)
ever goes off screen, the frog dies as well.
Testing Your Code
You are now able to successfully play any level in the game,
including 'complete.json'. Try them all out. Feel free to make some
new levels. Enjoy your accomplishment.
Pacing Yourself
If you want an A on this assignment, we suggest that you finish
by Tuesday, December 15, just after the second week. This will
give you enough time to complete the final task and add
some extensions if you wish.
Make sure that all of Task 2 is complete and tested before moving
on. If you have any bugs now, they can make the next part much
harder.
Completing this activity successfully will guarantee you a B+
on this assignment.
Task 3: Animation and Polish
The game is now playable, but it feels very rough. The frog
teleports across the screen rather than moving smoothly. And if the
frog dies, it just disappears. Most of the time we have no idea even
why we died.
To improve the game we need to add some juice, or what game
academics call game feel. These are features that do not
fundamentally alter how the game plays, but make the game feel
more professional or satisfying.
That is also the point of the extensions. If you have reached this
far in the assignment, you are allowed to start adding extensions to
the game. We will ignore any extensions for any submission that
does not complete the basic game. But from this point on you can
implement extensions for minor extra credit, including
compensating for occasional bugs in the first two tasks.
Tighten the Hitboxes
Before you can start working on animation, we need to talk about
hitboxes. A hitbox is the part of an image that can collide with a
game object. This is important because images often have
transparencies around them, and we do not want the transparent
parts to count as part of the collision.
This is particularly true when we start to animate the frog. Because
the frog can stretch out, this will mean that there is a huge
difference between the hitbox and the image size. Below we show
the difference between the images 'frog1.png' which you have used
so far and 'frog2.png' which you will use to animate the frog. The
image for 'frog2.png' has a huge amount of blank space because
we need to give the frog space to stretch out.
Hitbox Comparisons for FROG_IMAGE and FROG_SPRITE
Handling hitboxes can be really hard. Fortunately, you do not have
to worry about this. The game2d package does this automatically for
you. All you have to do is tell it what the hitboxes are, and the
methods collides and contains do the rest.
Defining the Hitboxes
All hitbox information is in a file called 'objects.json'. This is the lone
file in the JSON directory that is not a level file. Look at this file and
you will see that it has the image size and hitboxes for every single
image file provided. These files are divided
into 'images' and 'sprites'. You will work with sprites when
you animate the frog.
You have to understand what the hitbox information means. Let us
look at 'car1.png' as an example. Its hitbox is [1,5,1,5]. You read
these values as adjustments for the left, top, right, and bottom
edges, in that order. So the hitbox starts 1 pixel to the right of the
left edge, 5 pixels down from the top edge, 1 pixel left of the right
edge, and 5 pixels above the bottom edge. This is shown in the
image below.
The Hitbox for Image 'car1.png'
However, you do not need to understand any of this (unless you
want to add your own images). All you need to do is assign it. And
if you look at the documentation for GObject, you will notice that
that all have an attribute hitbox that has exactly this same format.
And this attribute is inherited by GImage.
Assigning the Hitboxes
So if all you have to do is assign the hitbox attribute, what is the
challenge? Well, you have to get this information from the
file 'objects.json' to the individual GImage objects. That means you
have to load this file at the same time you load the level file. And
you have to change all of your initialzers to include an additional
parameter for the hitbox dictionary.
But fortunately, once you do that, it is pretty straight forward. To
get the hitbox for an image, just use the ‘type' as a key.
Testing Your Code
If you look at the JSON directory, you will notice a file
called 'bigones.json'. This level includes the obstacles 'biglog' and
‘bigcar'. If you do not implement the hitboxes correctly, then the
second grey car will kill your frog even if the frog is sitting safely on
the grass. Furthermore, the frog will be able to ride along on the
second log even if it is not on top of it.
Fortunately, these two obstacles are included in 'objects.json'. If you
can play this level normally, then your hitboxes are working
correctly.
Pacing Yourself
This is another straight-forward activity. You just have to modify all
your initializers. Again, you should be able to do this in a day,
finishing by Wednesday, December 16. But make sure you make a
back-up of your code before making big changes like this. We
would hate for you mess up your progress and not know how to
get it back.
Animate the Frog
This is the big one. If you can get past this part, the last two parts
of the assignment are easy. It is time to animate the frog so that it
moves smoothly between lanes. This is where you are going to use
the coroutines that we talked about in Lesson 29. You will also get
some experience with these on lab 23 to aid you on this step.
Up until now, you have not done that much with the Frog class.
Maybe you added a visible attribute. Maybe you added some
attributes to the frog to remember the start position. But now you
are going to see why we created the Frog class, and did not just
leave it a GImage object like we did for the other obstacles.
Creating the Coroutine
The first thing we want to do is to slide the frog between lanes. The
way we are going to do this very close to what we do in the
coroutine _animate_slide in the file coroutine1.py in the
provided sample code. You should use FROG_SPEED to define how
long the coroutine runs. You will find that you no longer need the
cooldown attribute when you do this. You detect player input if the
frog is not animating and ignore it if it is.
The difference between the frog and coroutine1.py is that you need
to be prepared to move in different directions. While the frog slides
smoothly, it turns immediately as soon as the coroutine starts.
When you are done with this step, your frog movement should look
like this.
We recommend that you make the coroutine a method in
the Frog class. That way the coroutine has access to all of the frog
attributes and you do not have to pass them as parameters when
the coroutine starts.
There is one other important thing to keep in mind, and that is how
this sliding movement affects collisions. You do not have to do
anything different above cars. But logs are going to be a problem.
You die if you touch water without being on a log. But if the frog is
sliding, there is going to be a period in the jump when it is over the
water but not yet on the log.
You need to change the collision code for the Water class. The frog
only dies if it is in the water and animation has stopped. If the
animation is still active, you can assume that the frog is safely in
mid-air. Similarly, the log should only push the frog if the animation
has stopped. If the frog is still in the air, the log has no affect.
Finally, if the frog dies in the middle of an animation, you need to
clear the animator (set it to None). Otherwise, the frog will resume
that animation (probably half way across the board) when the
player continues.
Switching to a Sprite
The frog now moves smoothly. But we want to see it actually jump.
We are not going to be able to do that with a still image. We are
going to need a sprite. A sprite is a collection of images that you
flip through to create an illusion of movement. You will get
experience with one of these in lab 23.
Read the documentation for GSprite to see how it works. The
sprite image is defined by the constant FROG_SPRITE (though it is
missing the '.png'). If you use this as a key in the 'objects.json' file,
you will get the image file and the correct format (the number of
rows and columns).
You will also notice that GSprite objects have a hitboxes attribute.
This is different from the hitbox attribute (which they have as well).
This attribute defines the hitbox for every frame. If you look at the
frog sprite, you will see that they are wildly different. Once again,
this is all handled for you automatically. If you set the frame, then
the game will set the correct hitbox. You just need to assign
the hitboxes attribute at the beginning.
Animating the Sprite
While you are smoothly sliding the frog, you want to flip through all
of the sprite images as well. You change the sprite image by setting
the frame attribute. The frog is at rest at frame 0. You stretch out the
frog by increasing the frames until you get to frame 4 (this final
frame). Then you contract by decreasing frames in the reverse
order until you get to frame 0.
If you look at coroutine2.py in the provided sample code you will
notice that we do almost exactly the same thing when we turn the
ship. If you can figure out how that code works, then you can
animate the frog here.
Testing Your Code
As always, you should play your game. When you are done, your
movement should look something like this
Pay close attention to how your frog works with the water. You do
not want your frog dying in mid animation for no reason. Also pay
close attention to what happens when you continue after a death
or a successful exit. Is the frog back in its normal resting position in
the correct location? If not, you did not reset the animator correctly.
Pacing Yourself
We are budgeting two-days for this part, with a suggested finish
date of Friday, December 18. Honestly, we are not expecting a lot
of students to get this one. If you get it, congratulations. And if a lot
of people in the class gets it, even better. Everyone who gets this
far deserves an A (an exam-level A).
Completing this activity successfully will guarantee you a low
A on this assignment.
Animate the Death
It is still the case that if your frog dies it just disappears. That is not
satisfactory. We want to see what killed our frog. That is going to
require a dying animation. For this, you will need another sprite,
given by DEATH_SPRITE. We recommend that you manage this sprite in
the Frog class (so now the Frog class because a composite object).
But you are free to manage it with a second class in models.py if you
wish.
Either way, you should manage the death animation as a coroutine.
There can only be one animator at a time. Either the frog is moving
or it is dying. So this will be a lot like the two animators
in coroutine1.py in the provided sample code.
Creating the Death Sprite
The format and file for the death sprite is defined
in 'objects.json' using the DEATH_SPRITE key. You will notice that it has
no hitboxes. That is because the death sprite cannot collide with
anything (it is already dead). Thus it is safe for both
the hitbox and hitboxes attribute to be None.
While you can create the death sprite each time the frog dies, it is
better to create it once in the beginning, at the same time that you
create the frog. That is why we suggest turning the Frog class into a
composite. It displays either the frog or the death sprite, though
both will be invisible if the frog makes it to the exit. In addition, the
death sprite should be located exactly where the frog was when it
died. This is exactly the thing that you learned how to do with
composite objects in lab 22.
Once again, however, we will not be checking this aspect of your
code. If the death sprite works, then it works. We do not care how
you do it.
Animating the Death Sprite
The coroutine for the death sprite is even easier than the one for
the frog. It does not have to move. It only has to go from the first
frame to the last frame. The amount of time to animate DEATH_SPEED. If
you could animate the frog, this is simple.
The important feature, however, is that you should not pause the
game (and deduct the life) until the death animation is finished. If
you pause to early, the player will see the skull-and-crossbones,
but the death will not be animated.
Testing Your Code
When you are done, your game should look like the video at the
start of the instructions (minus the sound). You are almost done.
Pacing Yourself
This is much easier than the frog movement, so you completed
that task you should be able to finish this in a day. We suggest
finishing this by Saturday, December 19. This will give you one
day for the last task and then another day to check over your work.
Completing this activity successfully will guarantee you a solid
A on this assignment.
Play Sounds
If you watch the video above you will notice that we have sound
effects. The frog croaks when it jumps (CROAK_SOUND), splats when it
dies (SPLAT_SOUND), and trills when it reaches an exit (TRILL_SOUND).
To load an audio file, you simply create a Sound object as follows:
jumpSound = Sound(CROAK_SOUND)
Once it is loaded, you can play it whenever you want (such as
when the frog jumps) by calling jumpSound.play().
We are not going to tell you anything more than that. If you made it
this far, you can read the online specification to see how to
use Sound objects. You cannot replay a sound until the current
version of the sound stops. So if you want to play the same sound
multiple times simultaneously (which is only likely to happen if you
add extensions, you will need two different Sound objects for the
same sound file. Proper game audio can get really complicated
and this is one of the professor’s active areas of research.
Important: Loading sounds can take a while. We recommend that
you load all sounds you plan to use at either the start of the game
or the start of a level.
Pacing Yourself
You will want one last day to check all your work and go over
the finishing touches. Therefore, we recommend that you finish
this activity by Sunday, December 20.
Additional Features
If you are ahead of schedule, then you are welcome to do whatever
you want to extend the game and make it more fun. Doing so will
require that you not only add more code, but also that you make
your own level files. For example, you might want to add turtles to
the game. You will notice that we have provided a sprite sheet for a
submerging turtle in the IMAGES directory. We also have provided a
tasty fly for the frog to eat as a pickup. And what would Frogger be
without a score documenting how fast or how far you made it in
the game?
You are allowed to change anything you want so long the
program runs normally on the level files that were provided. If
you want to demonstrate anything new you have to define your
own level file to show it off. We recommend that you give this level
file a version number other than 1.0. In fact, you might want to
veersion them in the order that you want us to look at them.
If you have additional features in your game, we may award extra
credit for them.
If you do add any extensions, we ask that you add a README text
file to your submission. Document any extensions that you have
added and tell us what level files we should run in order to
experience them.
Finishing Touches
Before submitting anything, test your program to see that it works.
Play for a while and make sure that as many parts of it as you can
check are working. Cycle between multiple level files.
When you are done, reread the specifications of all your methods
and functions (including those we stubbed in for you), and be sure
that your specifications are clear and that your functions follow
their specifications. If you implemented extensions, make sure your
documentation makes it very clear what your extensions are.
As part of this assignment, we expect you to follow our style
guidelines:
• You have indented with spaces, not tabs (Atom Editor handles
this automatically).
• Classes are separated from each other by two blank lines
• Methods are separated from each other by a single blank line
• Class contents are ordered as follows: getters/setters,
initializer, non-hidden methods, hidden methods
• Lines are short enough that horizontal scrolling is not
necessary (about 80 chars)
• The specifications for all of the methods and classes are
complete
• Specifications are immediately after the method header and
indented
• No method is more than 30 lines long, not including the
specification
We are serious about the last one. This is a potential 10 point
deduction.
Turning it In
You are potentially modifying a lot of files in this assignment. At a
bare minimum, you are modifying app.py, level.py, lanes.py,
and models.py. You might be modifying consts.py. You might have
extra art and sound files.
To simplify the submission process, we are not asking you upload
each individual file. Instead, put all your files in a zip file
called froggit.zip and submit this instead. We need to be able to
play your game, and if anything is missing, we cannot play it.
However, make sure that your file is less than 100 MB. CMS cannot
take anything more than that and sound files can get very large.
If you did add any extensions, then your submission must include
a README file documenting all of the extensions you added. You
should also tell us what level files we should run in order to
experience them. We cannot give you credit for extensions if we do
not know about this. If there is no README file, we will assume the
game has no extensions.
While there are no test cases this time, you should be able to figure
out if everything is working simply by playing the game. There are
no tricky “restore everything to how it was” like with Turtles; no
nasty surprises lurking in the specifications. Just get the game
working.
Assignment Source Code
To work on this assignment, you will need all of the following:
The last item is a link that you should refer to through the
assignment, while the other two are files to download. Only the first
is a must download, as it contains the all of the source code
necessary to complete the assignment.
The second file is a collection of demo code from the lesson
on GUI programming, as well as the lesson on coroutines. This
sample code contains a lot of hints on how to approach some of
the harder parts of this assignment, and we reference these
samples throughout the instructions.
As with the imager application, this assignment is organized as a
package with several files. To run the application, change the
directory in your command shell to just outside of the
folder froggit and type
File Description
froggit.zip The application package, with all the source code
samples.zip Several programs that give hints on this assignment
game2d API The documentation for how to use the game2d classes
python froggit
In this case, Python will run the entire folder. What this really means
is that it runs the script in __main__.py. This script imports each of the
other modules in this folder to create a complex application. To
work properly, the froggit folder should contain the following:
For the most part, you only need to understand the first four files -
app.py, level.py, lanes.py and models.py - as well as the JSON directory.
The other files and folders can be ignored. However, if you decide
to add a few extensions, it helps to understand how all of these fit
together.
app.py
This module contains the controller class Froggit. This is the
controller that launches the application, and is one of four modules
that you must modify for this assignment. While it is the primary
controller class, you will note that it has no script code. That is
File Description
app.py The primary controller class for the application
level.py A secondary controller for a single playable application
lanes.py A collection of mini-controllers for the individual lanes
models.py Model classes for the game (i.e. Frog)
consts.py All module with all of the constant (global variable) values
game2d A package with classes that can display graphics on the screen
Sounds A folder of sound effects approved for your use
Fonts A folder of True Type fonts approved for your use
Images A folder of images for the frog and various obstacles
JSON A folder with different levels you can play
contained in the module __main__.py (which you should not
modify).
level.py
This module contains the secondary controller class Level. This
class manages a single level of the game. It works as a
subcontroller, just like the example subcontroller.py in the provided
sample code. It is another of the four modules that you must
modify for this assignment, and is the one that will require the most
original code.
lanes.py
Levels in Frogger consist of horizontal lanes that the frog has to
traverse. These could be roads, water, or even just a strip of grass.
And each of these lanes is very different from one another. The frog
does not die from touching a road, but will die if it is hit by a car on
the road. On the other hand, the frog dies in the water (it cannot
swim) unless it is touching a log.
This will make the code quite complicated. So to make things
much easier, you will make a class for each type of lane: road,
water, grass and (exit) hedge. Technically these classes will act as
subcontrollers to Level just as Level is a subcontroller for Froggit. In
complex applications there are many layers of controllers.
models.py
This module contains the model class Frog. This class is similar to
those found in the pyro.py demo in the provided sample code. If you
want to add other model classes (e.g. turtles or pick-ups), then you
should add those here as well. This is the last of the four files you
must modify for this assignment.
consts.py
This module is filled with constants (global variables that should
not ever change). It is used
by app.py, level.py, lanes.py and models.py to ensure that these
modules agree on certain important values. It also contains code
for adjusting your default level. You should only modify this file if
you are adding additional features to your program.
game2d
This is a package containing the classes you will use to design
your game. These are classes that you will subclass, just like we
demonstrated in the lesson videos. In particular, the class Froggit is
a subclass of GameApp from this package. As part of this
assignment, you are expected to read the online
documentation which describes how to use the base classes.
Under no circumstances should you ever modify this package!
Sounds
This is a folder of sound effects that you will use in the final
activity. You are also free to add more if you wish; just put them in
this folder. All sounds must be WAV files. While we have gotten
MP3 to work on Windows, Python support for MP3 on MacOS is
unreliable.
Fonts
This is a folder of True Type Fonts, should you get tired of the
default font for this assignment. You can put whatever font you
want in this folder, provided it is a .ttf file. Other Font formats (such
as .ttc, .otf, or .dfont) are not supported. Be very careful with fonts,
however, as they are copyrighted in the same way images are. Do
not assume that you can include any font that you find on your
computer.
Images
This is a folder with image files for the frog and the obstacles.
The GImage and GSprite classes allow you to include these in
your game. You can put other images here if you wish.
JSON
This is a folder with JSON files that define a level. You will turn
these into dictionaries and use them to place objects in your game.
Understanding these files will be a major portion of this
assignment. But you should look at them briefly to familiarize
yourself with these files.
Assignment Scope
As we explained in class, your game is a subclass of GameApp.
The parent class does a lot of work for you. You just need to
implement three main methods. They are as follows:
Your goal is to implement all of these methods according to their
(provided) specification.
Method Description
start() Method to initialize the game state attributes
update(dt) Method to update the models for the next animation frame
draw() Method to draw all models to the screen
start()
This method should take the place of __init__. Because of how Kivy
works, initialization code should go here and not in the initializer
(which is called before the window is sized properly).
update(dt)
This method should move the position of everything for just one
animation step, and resolve any collisions (potentially deleting
objects). The speed at which this method is called is determined by
the (immutable) attribute fps, which is set by the constructor. The
parameter dt is time in seconds since the last call to update.
draw()
This method is called as soon as update is complete. Implementing
this method should be as simple as calling the method draw,
inherited from GObject, on each of the models.
These are the only three methods that you need to implement. But
obviously you are not going to put all of your code in those three
methods. The result would be an unreadable mess. An important
part of this assignment is developing new (helper) methods
whenever you need them so that each method is small and
manageable. Your grade will depend partly on the design of your
program. As one guideline, points will be deducted for methods
that are more than 30 lines long (not including specifications or
spacing).
You will also need to add methods and attributes to the
class Level in level.py, the class Frog in models.py and the
classes Lane, Road, Water. and Hedge in lanes.py. All of these classes are
completely empty, though we have given you a lot of hints in the
class specification. You should read all these specifications.
As you write the assignment, you may find that you need additional
attributes. All new instance attributes should be hidden. You should
list these new attributes and their invariants as single-line
comments after the class specification (as we have done this
semester). For example, if the Level class needs to access the exit
locations in a Hedge lane, then you are going to need a getter for
these exits (and we give hints on how to do this).
While documentation and encapsulation is essential for this
assignment, you do not need to enforce any preconditions or
invariants. However, you may find that debugging is a lot simpler if
you do, as it will help you localize errors. But we will not take off
points either way, as long as the game runs correctly.
Assignment Organization
This assignment follows the model-view-controller pattern
discussed in the lesson videos. The modules are clearly organized
so that each holds models, the view, or a controller. The
organization of these files is shown below. The arrows in this
diagram mean “accesses”. So the Froggit controller accesses the
view and Level subcontroller. The Level controller accesses the
lanes, the view, and the models. And Lane and its subclasses
access the view and the modesl.
This leads to an important separation of files. Froggit is never
permitted to access anything in models.py and lanes.py. Level is never
permitted to access anything in app.py. The classes in lanes.py may
not access anythign in app.py or level.py. And finally, models.py cannot
access anything other than the view. This is an important rule that
we will enforce while grading. All of this is shown in the diagram
below
The Import Relationships
In addition to the four main modules, there is another module with
no class or function definitions. It only has constants, which are
global variables that do not change. This is imported by
the models module and the various controllers. It is a way to help
them share information.
When approaching this assignment, you should always be thinking
about “what code goes where?” If you do not know what file to put
things in, please ask on Piazza (but do not post code). Here are
some rough guidelines.
Froggit
This controller does very little. All it does is keep track of the game
state (e.g. whether or not the game is paused). Most of the time it
just calls the methods in Level, and lets Level do all the work.
However, if you need anything between lives, like a paused
message or the final result, this goes here. This is similar to the
class MainApp from the demo subcontroller.py
Level
This class does all the hard work. In addition to the initializer (which
is a proper __init__, not start), it needs its
own update and draw methods. This is a subcontroller, and you
should use the demo subcontroller.py (in the provided sample code)
as a template.
The most complex method will be the update and you will certainly
violate the 30-line rule if you do not break it up into helpers. For the
basic game, this method will need to do the following:
• Animate the frog according to player input
• Move all the obstacles in each lane
• Detect any collisions between the frog and an obstacle
• Remove the frog after a death or a successful exit
In our code, each one of these is a separate helper (though the
second one will actually be managed by the Lane class below). You
should think about doing this in your code as well.
The Lanes
There are four types of lanes in the game: road, water, grass, and
the hedge. The hedge is where the exits are placed. The road has
cars (and trucks). The water has logs to jump on. The grass is a
safe zone that gives you a rest.
Because each lane acts differently, you want to make a separate
class for each one. However, they do have a lot in common, so
they will all be subclasses of the class Lane. These classes already
appear in lanes.py with their specifications.
Each lane is essentially a mini-level. The frog needs to get
succesfully past it. That is why each lane will be its own
subcontroller, controlling its own lane. As a subcontroller, each lane
should have its own __init__, update, and draw methods. However,
most of these will go in Lane and the other classes will safely inherit
them. When the Level class needs to update the lanes, all it will do
is loop through the lanes and call the update method for each one.
The Models
The models just keep track of data. Most of the time, models just
have attributes, with getters and setters. Think Image from the
previous assignment. However, sometimes models have additional
methods that perform computation on the data, like swapPixel.
The models in this assignment are the game objects on screen: the
frog and any obstacles. Most of the time, you do not need a new
class for a module, as the class GImage does everything that you
needs for the cars, logs, and exits. However, the frog is very special
and so the frog will need a custom class. We have written the
specification of this for you.
If you want to add any additional features to your game, you may
need to add some new models here. For example, turtles that will
occasionally dive under water have to be animated in the same
way that the frog does. So they will need their own classes for
exactly the same reason. You should only add new classes for the
additional features.
Suggested Micro-Deadlines
You should implement the application in stages, as described in
these instructions. Do not try to get everything working all at once.
Make sure that each stage is working before moving on to the next
stage.
Set up a schedule.
Overview of Froggit
The layout of a Froggit game depends on the level file you are
using. We have included many different level files in
the JSON directory. The files easy1.json and easy2.json are easy games
to help you test your game, while roadsonly.json and complete.json are
a little more challenging. The level that you use is defined by the
variable DEFAULT_LEVEL in const.py. You can also change the level at
any time by specifying it when you run the game. For example, if
you type
python froggit roadsonly.json
it will play the game with the level roadsonly.json as the DEFAULT_LEVEL.
Below is the set-up for the complete.json to test out your game in the
end.
The Starting Position
All of the levels are arranged as a grid, where each grid square
is GRID_SIZE pixels on each side. The lanes are one grid unit in
height, and several grid units in width. Each exit is one grid unit
wide. Cars or logs can take up multiple grid squares, depending on
the specific obstacle. This grid is not explicitly visible, but it
becomes obvious once you play the game for a bit.
The Invisible Grid
Once the game begins, the cars and logs move across the screen.
They go in different directions, specified by the 'speed' key in the
JSON dictionary. Once they go off the screen, they wrap back
around to the other side. How fast they wrap around is defined by
the 'offscreen' key in the JSON dictionary.
The frog has to safely make it to (i.e. touch) one of the exits, which
is a lily pad at the final hedge. When that happens, the game puts a
bluish frog on the lily pad and restarts another frog at the starting
point, as show below. Exits may not be reused. The second frog
cannot use the exit that currently has a frog on it.
At the First Exit
If a frog dies, the game pauses and deducts a life from the player.
These lives are shown by the frog heads in the top right corner. In
the picture below, the game is paused after losing the first life.
Down One Life
There are two ways for the frog to die. The frog dies if it is hit by (or
even touches) a car. The frog also dies if touches the water. Why
this is a frog that cannot swim is a mystery from the early days of
video games. To survive on the water, the frog must hop on a log.
But the logs carry the frog with it when they move, making the
game a little more challenging.
Once you understand those rules, the game is simple. You win by
filling up every lily pad at the hedge. You lose if you run out of lives
before this happens. In the video below, we show both outcomes
on the level easy2.json.
Game State
One of the challenges with making an application like this is
keeping track of the game state. In the description above, we can
identity several distinct phases of the game:
• Before the game starts, but no level has been selected
• When the level has been loaded, but the frog and obstacles
are not moving
• While the game is ongoing, and the obstacles are moving
onscreen
• While the game is paused (e.g. to show a message)
• While the game is creating a new frog to replace the old one
• After the game is over
Keeping these phases straight is an important part of implementing
the game. You need this information to
implement update in Froggit correctly. For example, whenever the
game is ongoing, the method update should instruct the Level object
to move the frog. However, if the game has just started, there is
no Level object yet, and the method update should create one.
For your convenience, we have provided you with constants for six
states:
• STATE_INACTIVE, before a level has started
• STATE_LOADING, when it is time to load the level file
• STATE_ACTIVE, when the game is ongoing and the obstacles are
moving
• STATE_PAUSED, when the game is paused to display a message
• STATE_CONTINUE, when the player is waiting for a new frog
• STATE_COMPLETE, when the game is over
All of these constants are available in consts.py. The current
application state should be stored in a hidden
attribute _state inside Froggit. You are free to add more states if you
add extensions. However, your basic game should stick to these
six states.
The rules for changing between these six states are outlined in the
specification of method update in Froggit. You should read that in its
entirety. However, we will cover these rules in the instructions
below as well.
Level JSONs
All of the information about a game level is stored in a JSON file.
You should remember what a JSON string is from the very first
assignment. A JSON file is just a file that stores a very large JSON
string. These JSON files are a lightweight way to store information
as nested dictionaries, which is the standard way of sending
complex data across the Internet.
The tricky part about JSONs is coverting the string (or file) into the
Python dictionary. Fortunately, we have provided a tool that makes
this part easy. The GameApp class includes a method
called load_json which your Froggit class will inherit. Simply specify
the name of the JSON file. As long as that file is stored in
the JSON directory, this method will load this file and convert to ta
Python dictionary for you.
This means that working with level files is really all about working
with a complex nested dictionary. To understand how these
dictionaries work, look at the file easy2.json. This includes all of the
features that you will need to implement for the basic game. The
top-level dictionary has five keys:
• 'version' : A float representing the level version.
This version is only relevant if you define your own levels. It is
discussed in the section on additional features.
• 'size' : A list of two integers for the width and height.
The size represents the size of your game in grid squares, not
pixels. You multiply these by GRID_SIZE to get the size of the game.
• 'start' : A list of two integers for an x and y position.
The start value is the starting grid square for the frog (grid squares
start at 0 like any list). You multiply its two integers by GRID_SIZE to
get the starting pixel position for the frog.
• 'offscreen' : An integer to support “movement wrap”.
Objects need to wrap back to the beginning once they go off
screen. But you do not want to do that immediately, as the images
will snap and flicker. You want them to go completely offscreen
before you wrap them. This value is how far (in grid squares) that
any image must be offscreen before it is time to wrap it back
around. This is discussed in the activity to move the obstacles.
• 'lanes' : A list of all the lanes in the level.
The list of lanes should have the same number of elements as the
(grid) height of the level. The are ordered bottom up. The first lane
in the list is the bottom row, and the last lane in the list is the top
one.
The real nesting happens in these lanes. Each lane is its own
dictionary, with up to three keys. These are as follows:
• 'type' : The lane type.
The lane type is a string, and is one
of 'grass’, 'road', 'water' and 'hedge'. Unless you add new types of
lanes to the game, there are no other possibilities.
• 'speed' : A float indicating how fast obstacles move in this
lane.
All obstacles in a lane move at the same pace. This is the number
of pixels they move per second. Movement is left-to-right. If the
speed is negative, then the movement is right-to-left.
• 'objects' : The list of obstacles in this lane.
The exact obstacles vary by lane type. For a road, these are the
cars. For a water lane, these are the logs. For the hedge, these are
the exits (which technically count as obstacles).
Not all lanes have all three keys. Grass has no obstacles. The
hedge has exits, but they do not move and so they have no speed.
However, every lane is guaranteed to have a 'type', and it is
guaranteed to be one of those four values.
Finally, there are the obstacles in the list for 'objects'. All obstacles
have two keys:
• 'type' : A string representing the obstacle type.
For this assignment, it is the image file (minus the '.png’ suffix) for
this obstacle. So type 'log2' corresponds to the image file 'log2.png'
• 'position' : The float for the obstacle position.
Technically, the position represents a grid square, so you multiply it
by GRID_SIZE to get the position of the obstacle. However, obstacles
are different from the frog in the fact that they can actually be
between two grid squares (which is why this is a float).
If you understand all of these features, then you will have no
problem completing this assignment.
JSON Assumptions
One of the preconditions for this assignment is that level files are
properly formatted. That is, they are proper JSONs and they do not
have any important keys missing. As we said above, it is okay for
some keys to be missing, like 'speed' or 'objects'. But other keys
like 'size' and 'start' absolutely have to be there.
It is not your responsibility to enforce that the JSON files are in
the correct format. That is the responsibility of the level designer,
who is typically a different person on the team than a programmer.
However if you add any extensions to the game, then you will
likely be designing your own level files. And if you make mistakes in
your level files, you may cause your program to crash. Again, this is
a problem with the level file and not the game itself.
Task 1: Setup
We have divided these instructions into three parts. This task
involves setting up the game. That includes loading a level file and
using it to draw objects on the screen. While you will be able to
move the frog when you are done, nothing else will work. No
obstacles will move and you cannot win or lose the game.
We have made all of the instructions in this section very explicit. As
we move on to later tasks, the instructions will become a little more
vague.
Review the Constants
The very first thing that you should do is read the file consts.py. If
you ever need a value like the size of the grid, the initial size of the
game window, the frog movement speed, or so on, this is where
you go. When writing code, you should always use the constants,
not raw numbers (or “magic numbers,” as we call them). Magic
numbers make your code hard to debug, and if you make a change
(e.g. to make the grid size larger), you have no idea about all of the
locations in your code that need to be changed.
With that said, you are welcome to change any of these numbers if
you wish. You are also encouraged to add more constants if you
think of other numeric values that you need. Anytime that you find
yourself putting a number in your code, ask yourself whether or not
it would make sense as a constant.
Create a Welcome Screen
We start with a simple warm-up to get you used to defining state
and drawing graphics elements. When the player starts the
application, they should be greeted by a welcome screen. Your
initial welcome screen should start with two lines of text.
Because the welcome message is before any game has started, it
belongs in the Froggit class, not the Level class. You are already
seeing how we separate what goes where.
The welcomes will look something like the one above. It should tell
the player the name of the game, and tell the player to “Press ‘S’ to
Start”. You can change the wording here if you want. It could say
something else, as long as it is clear that the user should press a
key on the keyboard to continue. However, we recommend against
allowing the user to press any key, since in later steps that will
make it easy for the user to accidentally miss an important
message (or jump immediately in front of a truck).
To create a text message, you need to create a GLabel and store in
it an attribute. If you read the class invariant for Froggit, you will see
two attributes named _title and _text. The title attribute is the logo
of the game. The text attribute is for any messages to display to
the player. The text is typically smaller, like a footnote. While we will
never show the logo after the initial state STATE_INACTIVE, we will use
the text attribute for messages throughout the game.
Since the welcome message should appear as soon as you start
the game, it should be created in the method start, the first
important method of the class Froggit. When creating your
message, you will want to set things like the font size and position
of the text. If you are unsure of how to do this, look at the
class MainApp from the demo subcontroller.py in the provided sample
code.
As you can see from the documentation for GLabel and GObject,
graphics objects have a lot of attributes to specify things such as
position, size, color, font style, and so on. You should experiment
with these attributes to get the welcome screen that you want.
The first thing to understand is the positioning. In Kivy, the screen
origin (0,0) is at the bottom-left corner, and not the center like it
was for the Turtle assignment. If you want to find the center of the
window (our version centers these on the screen), the Froggit object
has attributes width and height that store the size of the window. In
addition, for the label object, the attributes x and y store
the center of the label. So you can center the label horizontally by
assigning x to width/2.
When placing label objects, you do not always want to center
them. Sometimes you would like the label to be flush againt the
edge of the window. For that reason we have attributes
like left, right, top, and bottom. These are alternate attributes
for x and y. They move the label in much the same way, but give
you a little more control over the positioning.
One you understand how to position the label, it is time to think
about your font choice and style. If you want to look exactlty like
the picture above, we have some constants to help you in const.py.
The font is ALLOY_FONT, which is a reference to AlloyInk). The title has
size ALLOY_LARGE while the message has ALLOY_MEDIUM. However, you are
not constrained to these choices. You may chose a different font or
font size if you wish, so long as it fits on the screen.
Drawing the Welcome Message
Simply adding this code to start is not enough. If you were to run
the application right now, all you would see is a blank white
window. You have to tell Python what to draw. To do this, simply
add the lines
self._title.draw(self.view)
self._text.draw(self.view)
to the method draw in Froggit. The (non-hidden) attribute view is a
reference to the window (much like the Window object in Assignment
4).Hence this method call instructs Python to draw this text label in
the window. Now run the application and check if you see your
welcome message appears.
Initializing the Game State
The other thing that you have to do in the beginning is initialize the
game state. The attribute _state (included in the class specification)
should start out as STATE_INACTIVE. That way we know that the game
is not ongoing, and the program should (not yet) be attempting to
animate anything on the screen. In addition, the other attributes
listed (particularly _level) should be None, since we have not done
anything yet.
The _state attribute is an important part of many of the invariants in
this game. In particular, we want your new attribute for the
welcome message to have the following invariant:
• If the state is STATE_INACTIVE, then there is a welcome message
with title and text.
• If the state is not STATE_INACTIVE, the _title attribute is None.
• If the state is STATE_ACTIVE, the _text attribute is None.
Does your start() method satisfy this invariant? Note the difference
between the last two invariants. That will become important later.
Dismissing the Welcome Screen
The welcome screen should not show up forever. The player should
be able to dismiss the welcome screen (and start a new game)
when he or she presses the S key. To respond to keyboard events,
you will need the attribute input, which is an instance
of GInput.This class has several methods for identifying what keys
are currently pressed.
When using the attribute input, remember the issues that we
discussed in class. The method update(dt) is called every 16
milliseconds. If you hold a key down, then you see a lot of key
presses. You just want the first press! That means you need some
way to determine whether or not the key was pressed this
animation frame and not in the previous one. See the state.py demo
from the sample code for some ideas on how to do this. This may
require you to add a new attribute to Froggit.
If you detect a key press, then you should change the
state STATE_INACTIVE to STATE_LOADING. This will load a level file and start
a new game. You are not ready to actually write the code to start
the game, but switching states is an important first activity.
Invariants must be satisfied at the end of every method, so you
need to assign None to both _title and _text now. This will require a
simple change to method draw() to keep it from crashing (you
cannot draw None). Once you have done that, run the application.
Does the message disappear when you press a key?
Documenting your New Attributes
When working on the steps above, you may have needed to add
new attributes beyond the ones that we have provided. Whenever
you a new attribute, you must add it and its corresponding
invariant as a comment after the class specification (these
comments are the class invariant). Add it just after the comment
stating ADD MORE ATTRIBUTES, to make it easier for the graders (and you)
to find them. We will deduct style points for instance attributes that
are not listed in the class invariant
Pacing Yourself
This first part of the assignment looks relatively straightforward, but
it gets you used to having to deal with controller state. Try to finish
this part by Wednesday, December 2, which is right after you have
had your first lab. This will give you time to familiarize yourself with
the online documentation, and make sure that you understand
how everything fits together.
Load the Level File
The next activity is a little more complicated than the welcome
screen, but once you complete it, you can be confident that you
have a good idea how everything fits together. The
state STATE_LOADING is only supposed to last one animation frame.
During that frame you should do the following:
• Load the DEFAULT_LEVEL into a dictionary
• Resize the window to match the level
• Create a new Level object
• Assign that Level object to the attribute _level
• Display the contents of the Level object
• Switch the state to STATE_ACTIVE
You do not need to worry about the details of STATE_ACTIVE for now.
However it is very important that you only create a
new Level object if the state is STATE_LOADING. If you continue to
create a Level object in STATE_ACTIVE, that will cause many problems
down the line (which you will not notice until much later).
Loading a JSON File
Loading a JSON file is easier than you think. There is a method
(technically it is a class method) in GameApp called load_json,
and Froggit inherits this method. Simply call this method on the
constant DEFAULT_LEVEL. The method will return a dictionary and you
are good to go. So from this point on, you will treat the level
information like a dictionary. Use the key 'size' to get the width and
height (in grid squares) for the level.
By default, the starting level is easy1.json. You can go into consts.py to
change this if you wish. Alternatively, you can test other levels by
typing
python froggit levelname
So you can load the 'complete.json' level simply by typing
python froggit complete.json
Resizing the Window
Different level files are different sizes. When you load a level, you
want to resize the window to match the level. The window width
should be the level width times GRID_SIZE. The window height should
be one grid size higher than the level height. That is because we
need an extra grid square to display the remaining lives.
To resize the window, remember that the Froggit object
has mutable attributes width and height. Just assign the value to
those. If you do it right, the levels 'easy1.json' and 'easy2.json' should
make the window slightly smaller, while 'complete.json' will not resize
the window at all.
Creating a Level Object
We have started the definition of the Level class for you in levels.py.
However, it does not do much, because we have not defined the
initializer. Furthermore, this means that the constructor does not
take any arguments. However, you want the Level constructor to
take a single argument: the JSON dictionary containing all of the
information about the level.
Inside of your initializer, your are going to take the 'lanes' list from
this dictionary and create the lane objects. For right now, each lane
is going to be a single GTile object. A GTile is an image that can be
repeated. To draw grass, road, or water, we take a single image
and repeat several times. For example, the image below shows the
difference between the image 'hedge.png' and 2x3 tiling of that same
image.
Normal Image vs 2x3 Tiling
To define a tiled image, you use the attributes x, y, width, height,
and source to specify how it looks on screen. The first four attributes
are just like GLabel, while source specifies an image file in
the Images folder. As with the label, you can either assign the
attributes after the object is created or assign them in the
constructor using keywords. Keyword arguments work like default
arguments in that you write param = value. See the online
documentation for an example of how to approach this.
The source for a lane tile is just the type plus the suffix '.png', So
the 'grass' type uses the image 'grass.png', the 'road' type uses the
image 'road.png' and so on. The height should be GRID_SIZE, while the
width should be the width of the window. The only hard part is
positioning the tiles.
The lanes are placed starting at the bottom of the screen and work
towards the top. There will be one blank line at the top, because
you set the height of the window to be GRID_SIZE higher than all of
the lanes. To place the lanes you might find it easer to use the
attributes left and bottom. These are alternate attributes
to x and y when you do not necessarily want to work with the
center of an object. For example, the first lanes
has left and bottom both 0, while the second lane has its bottom
at GRID_SIZE.
This suggests that you can create all of the lanes using a simple
loop in the initializer, adding them to the attribute _lanes as you go.
Drawing the Level Object
Once again, creating an GTile object is not enough to draw it on the
screen. But drawing the lanes is a bit more complicated than
drawing the welcome message. The lanes are (hidden) attributes
in Level. While the code
for lane in self._level._lanes:
lane.draw(self.view)
works (and you should try it out), it is not allowed. We will take off
major style points if a class ever accesses the hidden
attributes of an object of another class.
This is the purpose of adding a draw method to class Level.
The draw method in Froggit should call the draw method in Level, and
this in turn should call the the draw method for each lane (defined
in GObject).
However, only Froggit has access to the attribute view, which is
necessary for drawing. The class Level cannot directly access any
attributes in Froggit. If a method in Level needs an attribute
from Froggit, then Froggit must provide that attribute as an argument
in the method call. This means that the draw method in Level needs
to have view as a parameter, and the code in Froggit should look like
this.
self._level.draw(self.view)
Notice this is very similar to how we draw GObject objects.
Testing Your Code
While the concept of level files is a lot to wrap your head around,
the advantage is that they make it really easy to test your code. By
running your game on several different levels, you can see if you
did them correctly. Remember, to try out a different level, type
python froggit levelname
Here are what the backgrounds should look like for some of the
provided levels.
Background for easy1.json
Background for easy2.json
Background for roadsonly.json
Background for complete.json
WARNING: These files are designed assuming that you have a
1080p (1920x1080 pixel) monitor for your computer or laptop. If
you have a 720p monitor (1280x720 pixels), you will be able to play
the first two levels, but not the last two. In addition, we have found
that the default settings on the 13 inch MacBook are 1440x900
pixels making complete.png unplayable (though the others are fine).
However, you can solve this problem by going
into Settings > Display and choosing More Space.
Pacing Yourself
This is a longer activity, and we have budgeted up to two days to
work on this. That means you should try to finish this part
by Thursday, December 3,. However, if you take both of the days,
then you should complete the next activity on the same day.
Completing this activity successfully guarantees you will pass
(C-) this assignment.
Create the Frog
Next you need to create the frog. Again, this is to be stored in an
attribute of class Level. That means that you must create it in
the __init__ method of Level and modify your drawing code so that it
appears. The frog should be an object of class Frog, which is
included in models.py.
You will notice that Frog is a subclass of GImage. Like GTile,
a GImage object is used to draw an image to a screen. The difference
is that, if you specify the width and height, the GImage will resize the
image to fit entirely in that size; it will not tile the image. But
otherwise, creating and drawing a GImage is exactly the same as
creating and drawing a GTile object.
Initializing the Frog
Technically, we do not have to define an initializer for the frog. We
inherit the one from GImage already. But we do not want to use that
initializer. The image file for the frog will never change; it is defined
by the constant FROG_IMAGE in const.py. And the width and height will
be the default values. The only thing we want to specify is the frog
position on the grid.
So that means we want to define a custom initializer for
the Frog class. This initializer should have parameters for the start
position (both x and y) and that is it. This initializer then
calls super() to use the initializer for GImage, passing FROG_IMAGE as the
source file.
When positioning the frog, the center of the frog (defined by the
attributes x and y should be the center of the grid square.
Remember to multiply the grid positions by GRID_SIZE before passing
them to the initializer for GImage. The GImage class works with pixels,
not grid squares. But also remember that a grid square (col,row) is
not centered at (col*GRID_SIZE,row*GRID_SIZE). That would be
the bottom left corner of the grid square.
There is one more thing you need to do when you create the frog.
By default, the frog image is facing to the south. You need to set
the angle attribute of the frog to FROG_NORTH to make sure the frog is
facing in the correct direction.
One you have created the custom initializer for Frog, the initializer
in Level just needs to call the constructor for Frog and assign it to
the _frog attribute.
Drawing the Frog
Remember to draw the frog object in the method draw of class Level.
You draw the frog in the same way you drew the lane tiles. You do
not need to define a draw method in class Frog as it is inherited
from GImage. However it is important that you draw the frog last.
Objects are draw bottom to top and you want the frog to be on top
of everything.
Testing Your Code
Again, you can test your code by running each of the provided level
files. For all of the provided levels, the frog should be centered in
the bottom lane. For example, the level easy1.json should look like
this:
Frog for easy1.json
That is because 'easy1.json' has a start position of [5,0]. If instead
the start position where [0,5], it would look like this:
Frog for Swapped easy1.json
Pacing Yourself
This is super short activity. You should be able to complete this
soon after you finish drawing the lanes, which is by Thursday,
December 3. Finishing this on time will give you more time for the
next activity.
Completing this activity successfully will guarantee you a low
C on this assignment.
Add the Obstacles
Now we come to the first tricky point of the assignment. We want
you to create all of the obstacles in the level. This includes cars,
trucks, logs, and even exits. But do this you are going to need
to replace some of the code that you have already written. This is
something that we are going to be doing throughout the
assignment. You will start with simple code first to make sure
everything is working. But then you will go back and replace it with
more complex code.
The code you need to replace are the GTile objects you created in
the initializer for Level. That is because we are going to replace
those objects with a composite object. If you do not know what
composite objects are, these are discussed in lab 22, soon after
you get back. You may go over this lab with your lab instructor if
you need help.
For those who wish to skip the lab, composite object is when we
combine one or more GImage objects (or in this case a GTile object
and some GImage objects). The composite object is constructed just
like a subcontroller. It has an initializer that creates all of the objects
inside of the composite, and its own draw method to draw the
individual objects as well.
Initializing a Lane
The Lane object will serve as our composite object. You will notice
that there are classes for Grass, Road, Water and Hedge. However, you
can safely ignore all of those for now. We will only need those
classes later when we work on the basic game. Right now, the
parent class Lane will do all of our work for us.
Each Lane object will replace a single GTile. That means that
each Lane object should have an attribute _tile for the GTile that it
contains. Ideally, you can just copy the code from Level into the
initializer for Lane. However, that means you need to add some new
parameters to the initializer for the Lane. In addition to having
access to the JSON dictionary, the Lane initializer needs to
know which lane it is, so it can pick the right values out of the
nested dictionary. If you created the tiles with a for-loop, you
essentially need to pass the loop variable as a parameter to the
lane object.
Inside of the Lane initializer, you should also initialize all of the
obstacles, adding them to the list attribute _objs. You do this with a
for-loop just like you did the tiles in a previous activity. However,
this time all of the obstacles are represented by a GImage, not
a GTile. The source file for each obstacle is the same as the type
plus the suffix '.png'. So obstacle 'log2' corresponds to the image
file 'log2.png', and so on.
You do not need to specify the width and height of these images. The
default value is fine. However, you do need to specify their position.
If you look at the JSON files, you will notice that each obstacle has
a 'position' value. This is the grid square for the center of the
obstacle. So the y attribute of the obstacle should be the same as
the y attribute for the tile (the centers should match), but
the x attribute should be determined by the 'position'.
When using the 'position', note that obstacles can fill up more than
one grid square. For example, 'log2' is two grid squares in width.
So to put this obstacle in grid squares 1 and 2, its center will be at
grid 1.5 (so at the border of grid squares 1 and 2). This is shown
below.
Positioning an Obstacle
This means that you cannot get the x pixel by multiplying
the 'position' by the GRID_SIZE. But it is close, and you should be able
to figure out the correct answer now.
Orienting the Obstacles
In addition to placing the obstacles, you need to orient them
correctly. By default, all obstacles are facing the right edge of the
window, because the assumption is that they will move left-toright.
However, that is not always true. Some objects will move
right-to-left, and they will need to face in the opposite direction.
How do we know which is which? It depends on the 'speed' value of
the lane. If the speed is positive (or 0), then the obstacles face the
normal direction. But if the speed is negative, the obstacles face
the opposite direction.
Turning obstacles around is easy. We just rotate them 180 degrees
by senting the angle attribute in a GImage object to 180.
Constructing a Lane
Once you have the initializer defined for the lane, now it is time to
replace the code in Level. In the initializer for level, you want to
replace all of the GTile objects with Lane objects. We also
recommend that you use the right subclass for each type. So
a 'grass' lane should use a Grass object and so on. We know you did
not define any methods in these classes, but they inherit the
initializer from Lane. And breaking your lanes into specific classes
now will make parts of Task 2 much easier.
Drawing a Lane
Drawing a Lane object is similar to drawing Level object. You need to
add a draw method and it needs to take the view as a parameter. You
use this to draw the tile first (since it is at the bottom) and then all
of the obstacles in order.
If you do this correctly, you should not even need to change the
code in the draw method for Level. That code should still work.
Instead of calling the draw method of GTile, it is calling the method
for Lane.
Testing Your Code
Once again, you should try out your code on several different level
files to make sure that it is correct. Here are what the obstacles
should look like (with the frog included) for some of the provided
levels.
Obstacles for easy1.json
Obstacles for easy2.json
Obstacles for roadsonly.json
Obstacles for complete.json
Pacing Yourself
This is the hardest activity in the setup portion. Give yourself two
days to work on this activity, finishing it by Saturday, December 5.
Once you get this down, the rest of the setup will be easier.
Completing this activity successfully will guarantee you a C on
this assignment.
Display the Lives
The challenge of Frogger is that you have a limited number of lives
before the game is over. We want to display those lives to the
player. That is the purpose of the blank lane at the top of the
screen. When you are done with this activity, the top bar should
looke like this:
The Lives Counter
The lives are represented by GImage objects using the source
file FROG_HEAD. However, the FROG_HEAD image is very large. You will
need to scale it to GRID_SIZE width and height to get it to fit. You can
do that by setting the width and height attributes of the GImage object.
You should keep these GImage objects in a list. That makes drawing
them the same as drawing the lanes. And when you want to lose a
life, you will just remove one of the images from the list.
Finally, you should also include a GLabel indicating that these frog
heads represent the lives. If you use ALLOY_FONT, we recommend
using ALLOY_SMALL for the font size. In addition, make the right edge of
the label equal to the left edge of the first head.
Organizing Your Code
All of these attributes should be created in the initializer for Level.
However, you might notice that your initializer is starting to get a
little long. Remember the 30 line rule. If any method gets longer
than 30 lines, you might need to break it up into helpers. This may
now be the time to do that.
If you make helpers, remember to write specifications for all of your
helpers. You should also hide them. If another class does not need
access to them, they should be hidden.
Finally, remember to update the draw method to draw the label and
the frog heads. If you think that your draw method should be broken
up into helpers, you should do that as well.
Pacing Yourself
This is a short activity, though we have give you a day on it. Try to
finish this by Sunday, December 6. That should give you a
breather if you got behind on the previous activity.
Completing this activity successfully will guarantee you a high
C on this assignment.
Move the Frog
The last activity in the setup is to get the frog moving. We will not
worry about the obstacles yet. We just want to concentrate on the
frog. To move the frog, you will need to take into account the
player’s key presses. The frog only moves when the player presses
(or holds down) a key to make it move. By default, we assume that
the player will use the arrow keys to move the frog. However, if you
prefer WASD controls or some other control scheme, that is okay.
Updating the Frog
To see how to control the frog, you should look at the arrow.py demo
from the provided sample code. This example shows how to
check if the arrow keys are pressed, and how to use that to
animate a shape.
To perform the actual movement, you will need to add
an update method to Level. This will complete the methods turning it
into a proper subcontroller, like subcontroller.py in the sample code.
Note that moving the frog requires access to the input attribute.
This is an attribute of Froggit, not Level. The Level class will need
some way to access this.
The frog should move between grid squares. That means if the
player is pressing the up or down arrow, the frog will
move GRID_SIZE up or down. If the player is pressing left or right, the
frog will move GRID_SIZE left or right. As a result it will look like the
frog is slightly “teleporting” about the screen, as shown below:
Note that the frog also turns when it moves. Use the
constants FROG_NORTH, FROG_EAST, FROG_SOUTH and FROG_WEST to point your
frog in the correct direction.
If the player is holding down two keys at the same time, the frog
should not move diagonally. One of the keys should win (or they
should cancel out). Which one you do is up to you. We do not care.
Timing the Movements
If the player holds down an arrow key, we actually want the frog to
keep moving. So in a sense, moving the frog will easier arrow.py. We
do not care whether the player did or did not press a key the
previous frame. As long as the player holds down the arrow keys,
the frog will move.
However, part of the challenge of the Frogger is that the frog
moves slowly. If the frog moves a full GRID_SIZE every animation
frame, the frog will rocket off the screen immediately.
The solution to this it to introduce a cooldown. Once the frog
moves, you must wait FROG_SPEED seconds to move the frog again.
How do you keep track of time? That is the point of the dt attribute
in the update method of Froggit. That measures how many seconds
have passed. So, when the frog moves, set a cooldown attribute
to FROG_SPEED. Afterwards, subtract dt from the cooldown. Once this
reaches 0 or less, the frog can move again.
Restricting Movement
You should ensure that the frog stays completely on the board
even if the player continues to hold down a key. If you do not do
this, the frog is going to be completely lost once it goes off screen.
How do you do this? Look at where the frog will end up before you
move it. If it is going to go offscreen, then do not move
it. However if that movement required the frog to turn, you should
still turn the frog in place, even if it cannot move forward.
When we say “offscreen”, we mean the top bar with the lives
counter in it as well. As you can see in the video above, our frog
stops when it reaches the final hedge. However, the frog can still
move freely about the hedge. That will not be the case in the game;
the hedge will block the frog unless the frog is stepping on a lily
pad. But we will not worry about that until a later activity.
Testing Your Code
Testing this part of the code is easy. Run the game, and try to
move your frog. Does the frog move? Does it stay on the screen?
Then you have succeeded.
This activity is actually pretty straightforward, provide you
understand the arrows.py demo in the provided sample code. Most
people can complete this part of the assignment in less than two
hours. However, some people will find that they cannot get the frog
to move, even after properly adapting the code from arrows.py.
One of the key things is to make sure that your cooldown is
correct. Keep one of the arrow keys held down. The frog should
not move smoothly, but instead should only move
every FROG_SPEED seconds. It is possible to adjust this speed from the
command line if you wish. Try running
python froggit easy1.json 1
The last argument is a number which replaces FROG_SPEED. In this
case, the frog would move once a second, which is much slower
than the default 4 times a second.
Debugging Your Code
If your frog refuses to move, add a watch statement to
the draw method of Level. Print out the id (the folder name) of the
frog. Run the game and look for this print statement. What you see
will (hopefully) identify your bug.
The print statement never appears: In this case, you have forgot
to call the update method for Level from Froggit. Make
sure Froggit has this line of code in its update:
self._level.update(self.input)
The frog id keeps changing: In this case, you are accidentally
reseting the Frog object each frame. Go back and look at the
instructions for loading the level file to see how to stop this.
The frog id is constant: If this is true and the frog still will not
move, we have no idea what your problem is. It is something
unique. Please see a consultant immediately. They will not look at
your code, but they will help you debug it better. In our experience,
when students claim the frog id is not changing, they are wrong.
Pacing Yourself
We highly recommend that you complete this by Tuesday,
December 8, ending the first week of work. This will get you to the
first grade border.
Completing this activity successfully will guarantee you a C+
on this assignment.
Task 2: The Basic Game
When you are done with this task, you will have a complete and
playable game. It will not be fancy, but it will be playable. That will
be enough to put you at the B+/A- border.
Because we expect more of B students than we do of C students,
these instructions will not be as detailed as they were for the
previous task. At this point, we will have assumed that you have
read all the documentation and are familiar with how
the game2d objects work.
Detect the Exits
In the previous activity, the frog could walk all along the hedge.
This is not what we want. The frog should be able to enter an exit
(one of the lily pads). Any other part of the hedge will block the frog
from moving, just as if it were going offscreen.
To do this, we need to be able to do two things
• Detect if the frog is trying to walk into a hedge
• Detect if the frog is trying to walk into an exit (or an opening)
To do this, we will leverage two different methods inherited
from GObject: collides and contains. They are both very similar, but
they do have some important differences.
Detecting Movement into a Hedge
The collides method is used to determine when two game objects
(like say a frog and a background tile) are overlapping. To
determine if the frog is in the hedge, simply call the
method collides from either the frog or the hedge tile (it does not
matter which). If the result is True, then the movement is (potentially)
blocked. Otherwise, the frog is not trying to move into the hedge
and there is no problem.
Because the tile object is a (hidden) attribute of Lane and the frog
object is a (hidden) attribute of Level, we need some way to
compare these two together. This will require additional methods in
the Lane class. You either need a getter to access the tile, or a
collision method that allows the Level to pass the frog as a
parameter. We do not care which one you chose.
When you write this method, be prepared for there to be multiple
hedges. We have added an image 'open.png' which represents an
opening in the hedge that is not a exit. This allows the player to
walk through the hedge without reaching an exit. This is for
advanced levels like 'multihedge.json'. While supporting this file is
not necessary for this activity, we will be including it when we
grade this task at the end.
Detecting Movement into a Exit
Movement into a hedge is only allowed if it is into an exit (or an
opening). As the “obstacles” of a hedge are all either exits or
openings, this means that we just have to check whether the frog
collides with an obstacle in the hedge. If so, the frog can move into
the hedge.
However, this time we do not want you to use the collides method.
For reasons that will become clear when you give the frog a log
ride, we want to ensure that most of the frog overlaps the exit. So
that is the purpose of the contains method. The contains method
checks if a point (represented as a tuple (x,y)) is located inside of a
game object. We only want to allow movement into the hedge if
the center of the frog will be contained in the exit.
To do this, you loop though the exits and opening (the obstacles)
and see if the center of the frog is contained in one of them. If so,
the movement is allowed. Adding this functionality will require more
methods. But this time we do not want you to add these methods
to Lane. This is functionality that is specific to a hedge. So these
methods (even if they are just getters to access all of the exits)
belong in the Hedge class. This is where we are going to start to use
our subclasses, as each of the lanes will behave differently from
each other.
Testing Your Code
Once again, test your code by trying out some of the levels. You
should be blocked trying to walk into a hedge unless you are
walking into an exit or opening.
You should also try out the level 'multihedge.json' and see if you can
squeeze through the first hedge to make it to the second hedge. It
is okay if you walk through exits like they were an opening. You will
solve that problem in a later activity.
Pacing Yourself
You should be able to finish this activity in a single day. If you are
on schedule, this would be by Wednesday, December 9. This
activity is not that much more difficult than the normal restrictions
on frog movement. The only challenge is spreading out your code
among the individual classes.
Completing this activity successfully will guarantee you a B- on
this assignment.
Move the Obstacles
Up until now, the obstacles have just been sitting there. It is time to
get them moving. Movement is determined by the 'speed' value for
the lane. This is the number of pixels that each obstacle should
move per second. All obstacles in a lane move at the same rate. If
the speed is positive, they move left-to-right (so you add the speed
to the position). If the speed is negative, they move right-to-left.
Implementing Basic Movement
To implement the basic movement, you will need to add an update
method to Lane, also turning it into a proper subcontroller.
This update method should move all of the obstacles in the lane, and
it should be called from the update method in Level.
Technically, you should only move obstacles in road and water
lanes. However, hedges never have a speed (the exits do not move
unless you are making than an extension). And the grass has no
obstacles (again, unless you are adding an extension like snakes).
So that is why it is safe to add this code to the Lane class. Keep in
mind that this movement will require some additional attributes
(such as the lane speed).
When you move the obstacles, remember that the speed is the
number of pixels per second. To get the number of pixels to move
any given animation frame, you should multiply that value by dt, the
time since the last animation frame.
Implementing Wrap Around
Eventually the obstacles are going to go offscreen. When that
happens, we want to wrap them back around to the other side. So
objects going offscreen to the left should come back around on the
right, and objects going offscreen to the right should come back
around to the left.
Naively, the way to do this is to look at the x position of the
obstacle. If it is less than 0, set it the width of the window. If it is
greater than the width, set it to 0. However this solution causes a
major problem: “snapping”. The x attribute is the center of the
object. So we will see have of the object immediately disappear on
one side, and the other half teleport in on the other side. We want
the movement to be smooth.
We could try to solve the snapping problem by using
attributes left and right instead of x. If the left attribute is fully off
the right of the screen, we set the right attribute to 0. And similarly
if the right attribute is less than 0, we set the right attribute to the
width. This gets rid of snapping and the movement looks smooth.
However, it completely screws up spacing. Different obstacles have
different lengths (look at level 'easy2.py'). Over time, this will alter
the spaces in-between the obstacles changing the layout of the
level. In some cases, it could cause the obstacles to start to
overlap.
We want a solution that eliminates snapping but also preserves
spacing. The solution is to have an offscreen buffer. This is a little
bit of a distance that all objects are allowed to go offscreen. If the
buffer is 4, then obstacles can have an x as low as -4*GRID_SIZE and
as high as width+4*GRID_SIZE. When the x attribute crosses this
threshold, then the object should wrap around to the other
offscreen edge. Hence an object will traverse two offscreen buffers
before it comes back on screen.
The buffer should be large enough to support the largest obstacle
in the level. That is why it is defined inside of the level file. It is not
your responsibility to ensure that the buffer is large enough. That is
the responsibility of the level designer.
An Offscreen Buffer of Size 2
As the illustration above shows, the the object should not snap to -
buffer*GRID_SIZE or width+buffer*GRID_SIZE when it wraps around. If you
do this, you will still start to lose spacing over time because the
obstacles do not align exactly with the grid squares. Instead, when
the center of the obstacle (marked by the blue dot) crosses the
buffer, we determine the distance d that it went over. When we
teleport the image to the other side, we shift it forward by the
amount d.
This all sounds harder than it is. You can accomplish all of this with
a simple if-statement.
Testing Your Code
Once again, you can test your code on any level file. However, we
highly recommend that you test it on the level 'complete.json'. If it is
working properly, then it should look like the video below.
Let the game run for a long time. If you are losing spacing, then
you will start to see it in this level, because there are so many
obstacles.
Pacing Yourself
The code is getting harder, but we still think you can finish it in a
day. If you have made it this far then you understand more and
more of how the code works, and can write code faster. Therefore,
you should try to finish this activity by Thursday, December 10.
Completing this activity successfully will guarantee you a low
B on this assignment.
Squash the Frog
Now we have everything moving, but the game is not very
interesting. That is because nothing really interacts with anything.
Cars do not squash the frog. Logs do not take the frog for a ride.
Only hedges really do anything right now, but it does not feel like a
game.
In this activity, you will introduce the first challenge in your game:
cars. If a frog collides with a car, then the frog should die. Before
we talk about how to kill the frog, we first need to think about how
to detect a frog death. In many ways this is exactly the same
problem as detecting the exits. We need to add special methods to
the Road class that allow us to determine if a frog has collided with a
car. Look at how you solved that problem and come up with
something similar here.
Killing the Frog
What does it mean to kill the frog? Two things. First of all, the frog
should disappear from the screen. There are many ways to do that.
One is simply to set the _frog attribute to None, and make sure that
the Level does not draw the frog when it is None (just like
the Froggit class handles the welcome message). Another approach
is to add a visible attribute to the frog. When this attribute is False,
the frog is not drawn. This would require you to override
the draw method in Frog, instead of simply inheriting the one
from GImage. Choose whichever approach you are most comfortable
with.
The second thing about killing the frog is that you should pause the
game immediately. That is, the Froggit object should switch its state
to STATE_PAUSED (remember states?) and it should no longer update
the Level object until it is active again. That means you will need
some way for the Froggit app to know whether or not to pause the
game. You could add a getter to Level for Froggit to get this
information. Or you could turn the update method in Level from a
procedure into a fruitful method, and use the return value to
indicate whether or not to pause. Again, we do not care which
approach you take.
Pausing the Game
Pausing the game is easy. Simply do not call update in Level. Again,
you can see exactly this idea if you examine subcontroller.py in
the sample code.
However, there is one more thing to do. We want you to display a
message that the game is paused to the player, just like we have
done below.
Paused Game
Notice that we are displaying a message to the player. We can do
this with _text attribute in Froggit, provided that we draw it last.
There is not need to add a label to Level for this, especially
since Froggit is in charge of pausing.
In the example above we made a few stylistic choices that you
might want to adopt, but you are not required to do so. First of all,
we made the message neatly fit in a center lane of the level (which
is easy if you use ALLOY_SMALL). Furthermore, we set the background
of the GLabel to a solid cover, covering up the obstacles
underneath. We did all of this to make the label more readable. In
the end, this is all that matters. You can do whatever you want with
your paused message so long as it is still readable.
Some of the more observant of you might realize that the message
can get really tight as the levels get small. The level 'easy1.json' is
pretty narrow, but what if we have a level that is just three grid
squares wide? Actually, this is a precondition that we are going to
add to the game. No level will be less than 10 grid squares wide
and less than 8 grid squares high. So as long as your message fits
in that space, you are fine.
Resuming the Game
The player should be able to start playing again by pressing a key.
Our solution uses the ‘C’ key, but it can be anything you want so
long as the message you displayed makes it clear. Once again, we
recommend that you do not allow the player to press any key, as a
player holding down the up arrow will immediately move forward
and step in front of a truck.
This step is almost the same as STATE_INACTIVE. You detect a key
press and then switch the state. But this time you do not want to
switch to STATE_LOADING; we do not want to load a new level. Instead,
go to STATE_CONTINUE.
The purpose of STATE_CONTINUE is to reset the level. That means
making the frog visible again and putting the frog back in its start
location. If you forgot where the start location was, you might need
an attribute somewhere to remember it. Like STATE_LOADING, this is a
state that lasts exactly one animation frame. When it is done, it
should immediately switch to STATE_ACTIVE and the game should
continue as normal.
Testing Your Code
It is time to start killing frogs! Load up a level and step in front of
cars. You do not lose any lives yet, so it is harmless. Make sure
that you die when you touch a car and live when you avoid them.
Keep in mind that images have a little bit transparency around the
edges, but the collision code in GObject simply compares the size of
the images (which are rectangles). So it is possible that your frog
just barely escapes the car but the game still registers it as a touch.
This is a problem you will solve when you tighten the hitboxes.
Pacing Yourself
You should try to finish this by Friday, December 11. A lot what
you need to do here is similar to what you had to do for the exits.
The only new and tricky part is pausing and unpausing the game.
Completing this activity successfully will guarantee you a lowto-mid
B on this assignment.
Lose the Game
Now that you can kill your frog, you can lose the game. Every time
that the frog dies, you should remove one of the frog heads from
the top of the screen. Since this is just a list of GImage objects, this
should be easy. The problem is what happens when there are no
lives left.
Normally when the frog dies, Froggit is supposed to switch
to STATE_PAUSED. However if Froggit detects that there are no lives left
(this will probably require a new method in Level) then it should
instead switch to STATE_COMPLETE and display a very different message
to the player
Game Lost
For the basic game STATE_COMPLETE is the final state. There is no other
state to switch to. The player is expected to close the window and
relaunch the game if they want to play again. If you want to allow
the player to play again, that is an extension that you can add.
Testing Your Code
Once again, it is time to kill your frog. But when you run out of lives
the game should end. Since you cannot win yet, the only thing you
can do is die. This says something about the inevitability of our
mortality.
Pacing Yourself
This activity should be really quick. If you know how to pause the
game, tracking lives and marking a loss is not that much different.
However, we budgeting a full day for this, up to Saturday,
December 12. That is to allow you to catch up you got behind on
previous activities.
Completing this activity successfully will guarantee you a mid
B on this assignment.
Reach the Exits
You are already detecting the exits in the game. But that is not the
same as reaching safety. When a frog touches an exit (as
determined by the contains method), you need to put the frog safely
on the lily pad. That means putting the image FROG_SAFE on top of the
lily pad. You should also make the normal frog invisible and pause
the game, just like when the frog got hit by a car. But this time, the
player should not lose a life.
As when the frog dies, the player will continue once they press the
correct key. The frog will be reset to the starting position to begin
again.
Blocking the Exits
For the most part, this activity is not that much different that
getting hit by car. It is just that the frog does not lose a life.
However, there is one important additional detail. The frog can only
use an exit once. Once a lily pad has been taken, it cannot be used
again in the future. If a frog tries to step into an occupied lily pad,
the frog should be blocked, just like the frog is blocked by a normal
hedge square.
This is going to require a lot more attributes in the Hedge class. In
fact, you are going to want to add some attributes just do draw
the FROG_SAFE images on top of the lily pads. But you also might want
something else (like a list or dictionary) to keep track of which exits
are occupied and which are not.
More attributes means you need to define an initializer in
the Hedge class. It should add the new attributes, but use super() to
make sure that you inherit all of the original attributes from Lane. You
will also need to modify the methods you wrote when you
first detected the exits to account for these changes. Of all
the Lane subclasses, Hedge will prove to be one of the most complex.
Testing Your Code
You are almost ready to play a complete game. Try out the default
game 'easy1.json'. See if you can fill up all of the lily pads without
losing too many lives. If you want more of a challenge, try
‘roadsonly.json' instead.
Pacing Yourself
You should try to finish this by Sunday, December 13, which is
one day ahead of the last activity. We starting to move very fast
now. But students who reach this far should have been able to
finish the other parts a little early and hopefully have some time to
spare in their schedule.
Completing this activity successfully will guarantee you a midto-high
B on this assignment.
Win the Game
Winning the game is lot like losing the game. When you run out of
exits, the game pauses, Froggit switches to STATE_COMPLETE and
displays a congratulatory message like the one shown below.
Game Won
The trick is determining whether the game is won or not.
Class Hedge should give you some method to determine if all exits
are occupied. But remember that it is possible for there to be more
than one hedge, so you need to check this against all hedges.
Testing Your Code
You are now able to successfully play any level that does not
include water. Try out 'easy1.json’ first, as that is very easy level to
win. Try out 'roadsonly.json' for more of a challenge.
Finally, try out 'multihedge.json' to make sure that you work correctly
when you have more than one hedge. Because occupied lily pads
prevent the frog from going through the hedge, this level should
work correctly so long as you did not think that the open square
was an exit.
Pacing Yourself
This activity is very short and should be completed by Sunday,
December 13, the same day as the last activity.
Completing this activity successfully will guarantee you a high
B on this assignment.
Splash the Frog
The game is complete as far as roads are concerned. But the
standard game of frogger also has water. To try out the water
levels, we suggest that you switch DEFAULT_LEVEL to easy2.json. You will
be able to win and lose this level, but the water portions will be
boring. The frog will walk on water and not interact with the logs.
Taking a Log Ride
The frog should go for a ride if it mostly collides with a frog. You
should use the same rule that you use for exits. That is, use
the contains method on a log to check if the center of the frog is
inside the log. When that happens, we say that the frog is “on top
of the log”.
As long as the frog is on top of the log, the log should push it. That
means however much much the log changes its x attribute each
frame, the frog x attribute should change the same amount. When
the frog moves again, this will stop (unless jumps on top of a log).
Right now, movement is discrete. The frog jumps between lanes,
but is always either fully inside of or fully outside of a lane. So that
makes the code somewhat simple. Check which lane the frog is in.
If it is a water lane, find the log underneath the frog (if any). Then
move the frog by that amount.
Notice that this is going to screw up the grid movement. As a frog
gets pushed, it becomes like an obstacle and is no longer
guaranteed to be in a grid square. But because we are using
the contains method to reach the exit, everything will be fine.
Drowning the Frog
The Frogger frog cannot swim. Maybe its mother never taught it
how. What this means is that if the frog enters a water lane but it is
not on top of any log, it dies immediately. You should try it exactly
as if the frog were hit by a car. The game pauses and the player
loses a life. If this is the last life, the player loses the game.
In addition, the frog cannot go offscreen. While you previously
wrote code to prevent this from happening, that was only for the
arrow keys. There is nothing to keep a log from dragging a frog
offscreen. If the center of the frog (defined by the x and y attributes)
ever goes off screen, the frog dies as well.
Testing Your Code
You are now able to successfully play any level in the game,
including 'complete.json'. Try them all out. Feel free to make some
new levels. Enjoy your accomplishment.
Pacing Yourself
If you want an A on this assignment, we suggest that you finish
by Tuesday, December 15, just after the second week. This will
give you enough time to complete the final task and add
some extensions if you wish.
Make sure that all of Task 2 is complete and tested before moving
on. If you have any bugs now, they can make the next part much
harder.
Completing this activity successfully will guarantee you a B+
on this assignment.
Task 3: Animation and Polish
The game is now playable, but it feels very rough. The frog
teleports across the screen rather than moving smoothly. And if the
frog dies, it just disappears. Most of the time we have no idea even
why we died.
To improve the game we need to add some juice, or what game
academics call game feel. These are features that do not
fundamentally alter how the game plays, but make the game feel
more professional or satisfying.
That is also the point of the extensions. If you have reached this
far in the assignment, you are allowed to start adding extensions to
the game. We will ignore any extensions for any submission that
does not complete the basic game. But from this point on you can
implement extensions for minor extra credit, including
compensating for occasional bugs in the first two tasks.
Tighten the Hitboxes
Before you can start working on animation, we need to talk about
hitboxes. A hitbox is the part of an image that can collide with a
game object. This is important because images often have
transparencies around them, and we do not want the transparent
parts to count as part of the collision.
This is particularly true when we start to animate the frog. Because
the frog can stretch out, this will mean that there is a huge
difference between the hitbox and the image size. Below we show
the difference between the images 'frog1.png' which you have used
so far and 'frog2.png' which you will use to animate the frog. The
image for 'frog2.png' has a huge amount of blank space because
we need to give the frog space to stretch out.
Hitbox Comparisons for FROG_IMAGE and FROG_SPRITE
Handling hitboxes can be really hard. Fortunately, you do not have
to worry about this. The game2d package does this automatically for
you. All you have to do is tell it what the hitboxes are, and the
methods collides and contains do the rest.
Defining the Hitboxes
All hitbox information is in a file called 'objects.json'. This is the lone
file in the JSON directory that is not a level file. Look at this file and
you will see that it has the image size and hitboxes for every single
image file provided. These files are divided
into 'images' and 'sprites'. You will work with sprites when
you animate the frog.
You have to understand what the hitbox information means. Let us
look at 'car1.png' as an example. Its hitbox is [1,5,1,5]. You read
these values as adjustments for the left, top, right, and bottom
edges, in that order. So the hitbox starts 1 pixel to the right of the
left edge, 5 pixels down from the top edge, 1 pixel left of the right
edge, and 5 pixels above the bottom edge. This is shown in the
image below.
The Hitbox for Image 'car1.png'
However, you do not need to understand any of this (unless you
want to add your own images). All you need to do is assign it. And
if you look at the documentation for GObject, you will notice that
that all have an attribute hitbox that has exactly this same format.
And this attribute is inherited by GImage.
Assigning the Hitboxes
So if all you have to do is assign the hitbox attribute, what is the
challenge? Well, you have to get this information from the
file 'objects.json' to the individual GImage objects. That means you
have to load this file at the same time you load the level file. And
you have to change all of your initialzers to include an additional
parameter for the hitbox dictionary.
But fortunately, once you do that, it is pretty straight forward. To
get the hitbox for an image, just use the ‘type' as a key.
Testing Your Code
If you look at the JSON directory, you will notice a file
called 'bigones.json'. This level includes the obstacles 'biglog' and
‘bigcar'. If you do not implement the hitboxes correctly, then the
second grey car will kill your frog even if the frog is sitting safely on
the grass. Furthermore, the frog will be able to ride along on the
second log even if it is not on top of it.
Fortunately, these two obstacles are included in 'objects.json'. If you
can play this level normally, then your hitboxes are working
correctly.
Pacing Yourself
This is another straight-forward activity. You just have to modify all
your initializers. Again, you should be able to do this in a day,
finishing by Wednesday, December 16. But make sure you make a
back-up of your code before making big changes like this. We
would hate for you mess up your progress and not know how to
get it back.
Animate the Frog
This is the big one. If you can get past this part, the last two parts
of the assignment are easy. It is time to animate the frog so that it
moves smoothly between lanes. This is where you are going to use
the coroutines that we talked about in Lesson 29. You will also get
some experience with these on lab 23 to aid you on this step.
Up until now, you have not done that much with the Frog class.
Maybe you added a visible attribute. Maybe you added some
attributes to the frog to remember the start position. But now you
are going to see why we created the Frog class, and did not just
leave it a GImage object like we did for the other obstacles.
Creating the Coroutine
The first thing we want to do is to slide the frog between lanes. The
way we are going to do this very close to what we do in the
coroutine _animate_slide in the file coroutine1.py in the
provided sample code. You should use FROG_SPEED to define how
long the coroutine runs. You will find that you no longer need the
cooldown attribute when you do this. You detect player input if the
frog is not animating and ignore it if it is.
The difference between the frog and coroutine1.py is that you need
to be prepared to move in different directions. While the frog slides
smoothly, it turns immediately as soon as the coroutine starts.
When you are done with this step, your frog movement should look
like this.
We recommend that you make the coroutine a method in
the Frog class. That way the coroutine has access to all of the frog
attributes and you do not have to pass them as parameters when
the coroutine starts.
There is one other important thing to keep in mind, and that is how
this sliding movement affects collisions. You do not have to do
anything different above cars. But logs are going to be a problem.
You die if you touch water without being on a log. But if the frog is
sliding, there is going to be a period in the jump when it is over the
water but not yet on the log.
You need to change the collision code for the Water class. The frog
only dies if it is in the water and animation has stopped. If the
animation is still active, you can assume that the frog is safely in
mid-air. Similarly, the log should only push the frog if the animation
has stopped. If the frog is still in the air, the log has no affect.
Finally, if the frog dies in the middle of an animation, you need to
clear the animator (set it to None). Otherwise, the frog will resume
that animation (probably half way across the board) when the
player continues.
Switching to a Sprite
The frog now moves smoothly. But we want to see it actually jump.
We are not going to be able to do that with a still image. We are
going to need a sprite. A sprite is a collection of images that you
flip through to create an illusion of movement. You will get
experience with one of these in lab 23.
Read the documentation for GSprite to see how it works. The
sprite image is defined by the constant FROG_SPRITE (though it is
missing the '.png'). If you use this as a key in the 'objects.json' file,
you will get the image file and the correct format (the number of
rows and columns).
You will also notice that GSprite objects have a hitboxes attribute.
This is different from the hitbox attribute (which they have as well).
This attribute defines the hitbox for every frame. If you look at the
frog sprite, you will see that they are wildly different. Once again,
this is all handled for you automatically. If you set the frame, then
the game will set the correct hitbox. You just need to assign
the hitboxes attribute at the beginning.
Animating the Sprite
While you are smoothly sliding the frog, you want to flip through all
of the sprite images as well. You change the sprite image by setting
the frame attribute. The frog is at rest at frame 0. You stretch out the
frog by increasing the frames until you get to frame 4 (this final
frame). Then you contract by decreasing frames in the reverse
order until you get to frame 0.
If you look at coroutine2.py in the provided sample code you will
notice that we do almost exactly the same thing when we turn the
ship. If you can figure out how that code works, then you can
animate the frog here.
Testing Your Code
As always, you should play your game. When you are done, your
movement should look something like this
Pay close attention to how your frog works with the water. You do
not want your frog dying in mid animation for no reason. Also pay
close attention to what happens when you continue after a death
or a successful exit. Is the frog back in its normal resting position in
the correct location? If not, you did not reset the animator correctly.
Pacing Yourself
We are budgeting two-days for this part, with a suggested finish
date of Friday, December 18. Honestly, we are not expecting a lot
of students to get this one. If you get it, congratulations. And if a lot
of people in the class gets it, even better. Everyone who gets this
far deserves an A (an exam-level A).
Completing this activity successfully will guarantee you a low
A on this assignment.
Animate the Death
It is still the case that if your frog dies it just disappears. That is not
satisfactory. We want to see what killed our frog. That is going to
require a dying animation. For this, you will need another sprite,
given by DEATH_SPRITE. We recommend that you manage this sprite in
the Frog class (so now the Frog class because a composite object).
But you are free to manage it with a second class in models.py if you
wish.
Either way, you should manage the death animation as a coroutine.
There can only be one animator at a time. Either the frog is moving
or it is dying. So this will be a lot like the two animators
in coroutine1.py in the provided sample code.
Creating the Death Sprite
The format and file for the death sprite is defined
in 'objects.json' using the DEATH_SPRITE key. You will notice that it has
no hitboxes. That is because the death sprite cannot collide with
anything (it is already dead). Thus it is safe for both
the hitbox and hitboxes attribute to be None.
While you can create the death sprite each time the frog dies, it is
better to create it once in the beginning, at the same time that you
create the frog. That is why we suggest turning the Frog class into a
composite. It displays either the frog or the death sprite, though
both will be invisible if the frog makes it to the exit. In addition, the
death sprite should be located exactly where the frog was when it
died. This is exactly the thing that you learned how to do with
composite objects in lab 22.
Once again, however, we will not be checking this aspect of your
code. If the death sprite works, then it works. We do not care how
you do it.
Animating the Death Sprite
The coroutine for the death sprite is even easier than the one for
the frog. It does not have to move. It only has to go from the first
frame to the last frame. The amount of time to animate DEATH_SPEED. If
you could animate the frog, this is simple.
The important feature, however, is that you should not pause the
game (and deduct the life) until the death animation is finished. If
you pause to early, the player will see the skull-and-crossbones,
but the death will not be animated.
Testing Your Code
When you are done, your game should look like the video at the
start of the instructions (minus the sound). You are almost done.
Pacing Yourself
This is much easier than the frog movement, so you completed
that task you should be able to finish this in a day. We suggest
finishing this by Saturday, December 19. This will give you one
day for the last task and then another day to check over your work.
Completing this activity successfully will guarantee you a solid
A on this assignment.
Play Sounds
If you watch the video above you will notice that we have sound
effects. The frog croaks when it jumps (CROAK_SOUND), splats when it
dies (SPLAT_SOUND), and trills when it reaches an exit (TRILL_SOUND).
To load an audio file, you simply create a Sound object as follows:
jumpSound = Sound(CROAK_SOUND)
Once it is loaded, you can play it whenever you want (such as
when the frog jumps) by calling jumpSound.play().
We are not going to tell you anything more than that. If you made it
this far, you can read the online specification to see how to
use Sound objects. You cannot replay a sound until the current
version of the sound stops. So if you want to play the same sound
multiple times simultaneously (which is only likely to happen if you
add extensions, you will need two different Sound objects for the
same sound file. Proper game audio can get really complicated
and this is one of the professor’s active areas of research.
Important: Loading sounds can take a while. We recommend that
you load all sounds you plan to use at either the start of the game
or the start of a level.
Pacing Yourself
You will want one last day to check all your work and go over
the finishing touches. Therefore, we recommend that you finish
this activity by Sunday, December 20.
Additional Features
If you are ahead of schedule, then you are welcome to do whatever
you want to extend the game and make it more fun. Doing so will
require that you not only add more code, but also that you make
your own level files. For example, you might want to add turtles to
the game. You will notice that we have provided a sprite sheet for a
submerging turtle in the IMAGES directory. We also have provided a
tasty fly for the frog to eat as a pickup. And what would Frogger be
without a score documenting how fast or how far you made it in
the game?
You are allowed to change anything you want so long the
program runs normally on the level files that were provided. If
you want to demonstrate anything new you have to define your
own level file to show it off. We recommend that you give this level
file a version number other than 1.0. In fact, you might want to
veersion them in the order that you want us to look at them.
If you have additional features in your game, we may award extra
credit for them.
If you do add any extensions, we ask that you add a README text
file to your submission. Document any extensions that you have
added and tell us what level files we should run in order to
experience them.
Finishing Touches
Before submitting anything, test your program to see that it works.
Play for a while and make sure that as many parts of it as you can
check are working. Cycle between multiple level files.
When you are done, reread the specifications of all your methods
and functions (including those we stubbed in for you), and be sure
that your specifications are clear and that your functions follow
their specifications. If you implemented extensions, make sure your
documentation makes it very clear what your extensions are.
As part of this assignment, we expect you to follow our style
guidelines:
• You have indented with spaces, not tabs (Atom Editor handles
this automatically).
• Classes are separated from each other by two blank lines
• Methods are separated from each other by a single blank line
• Class contents are ordered as follows: getters/setters,
initializer, non-hidden methods, hidden methods
• Lines are short enough that horizontal scrolling is not
necessary (about 80 chars)
• The specifications for all of the methods and classes are
complete
• Specifications are immediately after the method header and
indented
• No method is more than 30 lines long, not including the
specification
We are serious about the last one. This is a potential 10 point
deduction.
Turning it In
You are potentially modifying a lot of files in this assignment. At a
bare minimum, you are modifying app.py, level.py, lanes.py,
and models.py. You might be modifying consts.py. You might have
extra art and sound files.
To simplify the submission process, we are not asking you upload
each individual file. Instead, put all your files in a zip file
called froggit.zip and submit this instead. We need to be able to
play your game, and if anything is missing, we cannot play it.
However, make sure that your file is less than 100 MB. CMS cannot
take anything more than that and sound files can get very large.
If you did add any extensions, then your submission must include
a README file documenting all of the extensions you added. You
should also tell us what level files we should run in order to
experience them. We cannot give you credit for extensions if we do
not know about this. If there is no README file, we will assume the
game has no extensions.