The latest instance of the course can be found at: O1: 2024
Luet oppimateriaalin englanninkielistä versiota. Mainitsit kuitenkin taustakyselyssä osaavasi suomea. Siksi suosittelemme, että käytät suomenkielistä versiota, joka on testatumpi ja hieman laajempi ja muutenkin mukava.
Suomenkielinen materiaali kyllä esittelee englanninkielisetkin termit.
Kieli vaihtuu A+:n sivujen yläreunan painikkeesta. Tai tästä: Vaihda suomeksi.
Chapter 3.6: Decisions, Decisions
About This Page
Questions Answered: How about some more practice on if
expressions? How can I make my game more unpredictable? How can
I write a game or simulation where I can move a character about
freely?
Topics: More selection with if
. Randomness. Private methods.
Optionally: the type o1.Direction
.
What Will I Do? Program. There are a few required assignments and quite a bunch of optional ones.
Rough Estimate of Workload:? The main assignments will probably take you less than an hour. The optional assignments are somewhat harder and take a substantial amount of additional time.
Points Available: A25.
Related Modules: FlappyBug, Odds. The optional assignments require MoreApps (new).
Assignment: FlappyBug (Part 13 of 17: An Obstinate Obstacle)
Foreword
In our current version of FlappyBug, we have only a single obstacle that glides across the view once. This is not quite satisfactory. It would be better if the bug had to continue dodging more obstacles.
Here are two alternative approaches:
Once the obstacle has moved out of sight beyond the left edge and is no longer “active”, we create another
Obstacle
object on the right-hand side of the view. We replace the obstacle that we had previously linked to theGame
with this new obstacle. The old obstacle object is no longer needed.Once the obstacle has moved out of sight beyond the left edge and is no longer “active”, we instantly move it to the right-hand side of the view simply by changing its coordinates. In a technical sense, the bug will keep encountering the same obstacle object over and over again, but in practice, it will look like new obstacles keep arriving from the right.
Neither approach is unambiguously better than the other. We will now pick the second approach, which is easy to implement.
Task description
Begin by adding an effect-free method isActive
in class Obstacle
. This method takes
no parameters. It returns true
if the obstacle’s right edge is located to the right
of the screen’s left edge, or if it’s exactly at the screen’s left edge. Otherwise,
the method returns false
. For example, if an obstacle has a radius of 50 and an x
coordinate of -50, the obstacle still just counts as active, but if the coordinate was
any less, it wouldn’t.
Then modify the approach
method in the same class. It currently looks like this:
def approach() =
this.currentPos = this.currentPos.addX(-ObstacleSpeed)
Edit the method so that it either shifts the obstacle left or teleports it back to the right, depending on circumstances. More specifically:
If the obstacle was active when the method is called, it moves leftwards as before.
If the obstacle wasn’t active, it gets a new x coordinate that is equal to the width of the visible game world plus 250 (i.e., 1250) and a new y coordinate that is equal to half the height of the game world (i.e., 200). What this means is that the obstacle will teleport a bit to the right of the visible game world, halfway between the top and the bottom edge, and resume its leftward movement there.
A+ presents the exercise submission form here.
Afterword
Now we effectively have multiple obstacles but they always appear at the same spot. This is not quite satisfactory. It would be better if the obstacles arrived at unpredictable heights.
How can we pick random coordinates for an obstacle?
This is a problem in its own right. Let us first consider randomness more generally.
Randomness
Class Random
Good news: Scala gives us a class called Random
that easily generates random numbers.
import scala.util.Randomval generator = Random(74534161)generator: scala.util.Random = scala.util.Random@1413cb7 generator.nextInt(10)res0: Int = 3 generator.nextInt(10)res1: Int = 8 generator.nextInt(10)res2: Int = 7 generator.nextInt(10)res3: Int = 2 generator.nextInt(10)res4: Int = 3 generator.nextInt(10)res5: Int = 2
A Random
object has an assortment of methods for generating
random numbers. One that is useful particularly often is
nextInt(n: Int)
, which returns a random number between
zero and n-1
.
For example, when we pass in 10, we get numbers between 0 and 9.
What’s that in the brackets, then? We’ll get to that momentarily.
You can try entering the above commands in the REPL. If you do, you’ll get exactly
the same list of random numbers from nextInt
: 3, 8, 7, 2, 3, 2, etc.
That makes little sense. After all, the numbers were supposed to be random.
Pseudorandom numbers
The generation of random numbers is too important to be left to chance.
—Robert Coveyou
The assignment “Pick 30 fully random numbers between 0 and 9.” is easier said than done. One practical option is to use a “good enough” mathematical algorithm for computing a sequence of numbers. For example:
Start by picking some number N, say N=20.
Take the Nth decimal of π. Let that be the first “random number”.
Determine the next “random number” by taking the another digit from π at 2*N.
Follow with 3*N, 4*N, and so on.
Randomness is hard not just to us humans but computers, too. A computer can’t simply pluck true randomness out of thin air.
The numbers produced by such an algorithm are pseudorandom numbers (näennäissatunnaisluku). Given the same “starting number”, known as a seed (siemen), we always get the same sequence of pseudorandom numbers. In our example algorithm, N was the seed; ever time we apply the algorithm with an N of 20, we get the same result.
Mathematicians have developed various algorithms that generate pseudorandom numbers.
Scala’s Random
class uses one such algorithm (which we won’t inspect more closely
in O1). We pass the seed as a constructor parameter (e.g., 74534161 above)
Given a particular seed, an algorithm always produces the same pseudorandom numbers. This can be either a blessing (if we wish to replicate a sequence that was originally chosen at random) or a curse (if we want a distinct sequence every time).
Seeding from the clock
There are different ways of picking a seed. In some cases, it can make sense to ask the end user to supply a seed number. Another approach is to take some “random enough” characteristic in the program’s environment; a classic trick is to take the computer’s current time and date to the precision of, say, a nanosecond, and seed the generator with that.
In many applications, it’s not terribly important to know which exact seed number is
being used. For that reason, package scala.util
provides not just the class Random
but also a singleton object of the same name. With this object, you can conveniently
generate clock-based pseudorandom numbers without explicitly instantiating class Random
:
import scala.util.RandomRandom.nextInt(10)res6: Int = 8 Random.nextInt(10)res7: Int = 6 Random.nextInt(10)res8: Int = 2
If you try these commands yourself you will (very probably) get different return values.
Assignment: FlappyBug (Part 14 of 17: An Unpredictable Obstacle)
Groundwork: randomLaunchPosition
Turning back to class Obstacle
, let’s add a method that picks a random starting
position beyond the right edge of the view. Here’s some starter code:
private def randomLaunchPosition() = val launchX = Add up the view’s width (1000), the obstacle’s radius, and a random integer between 0 and 499. val launchY = Pick a random integer that is at least 0 and less than the view’s height (400). Pos(launchX, launchY)
A couple of expressions have been left for you to fill in.
This method is private. It’s meant for use within the Obstacle
class only and isn’t part of the class’s public interface.
(Cf. private variables; Chapter 3.2.)
Task description
Implement
randomLaunchPosition
. Use the given template and fill in the missing parts.Modify the
approach
method so that it no longer places an inactive obstacle in a standard position. Instead, the method should callrandomLaunchPosition
to obtain a new position for the obstacle.Edit class
Obstacle
so that we no longer supply a starting location as a constructor parameter whenever we instantiate the class. Instead, we’ll pass in only the radius, and the new instance should call its ownrandomLaunchPosition
method to place itself at a random starting location.Make a small edit to class
Game
so that it’s compatible with the above change: pass just one constructor parameter to the newObstacle
.
Instructions and hints
Use the clock-seeded singleton object
Random
so that you get different results every time the game is played.You can also try creating a
Random
instance with a specific seed. If you do, you’ll give the obstacle the same “random” behavior every time.randomLaunchPosition
is a parameterless, effectful method, so we write the empty brackets()
, as per Chapter 2.6. (The method counts as effectful, because it affects the state of the underlying random-number generator.)
A+ presents the exercise submission form here.
Try some scary graphics
Discard rockPic
, which we’ve thus far used in the GUI to produce
a picture of the obstacle. Use Pic("obstacle.png"
) instead and
scale it to the right size with scaleTo(diameterOfObstacle)
.
Play if you dare.
More Assignments
Assignment: Odds (Part 9 of 9)
Return to class Odds
in the module of the same name. Add an eventHappens
method
that randomly determines whether an event occurs, assuming its likelihood is
represented by the Odds
object:
val dieRollOfSix = Odds(5, 1)dieRollOfSix: Odds = 5/1 dieRollOfSix.eventHappens()res9: Boolean = false dieRollOfSix.eventHappens()res10: Boolean = false dieRollOfSix.eventHappens()res11: Boolean = true
Whenever we call the method, we get either true
or false
at random.
In our die-rolling example, we get false
pretty much exactly five times
as often as we get true
.
Because it invokes the effectful method nextInt
, eventHappens
is effectful, too,
needs the empty brackets as a parameter list (Chapter 2.6). nextInt
counts as
effectful because it changes the random number generator’s internal state.
Remember to import Random
from scala.util
. And remember that Random.nextInt(n)
returns a number between zero and n-1
.
A+ presents the exercise submission form here.
Using the keyboard to trot about
Package o1.trotter
in module MoreApps contains a small application where you can
make a “trotter” — a virtual horse — run about against a background grid. The
trotter moves from one square to another as the clock ticks; it changes direction
when you press the arrow keys on your keyboard. Well, that’s what it’s supposed to
do, anyway.
Within the package, you’ll find the class TrotterGame
, which we’ll use as the domain
model. Feel free to study the whole class if you want, but for this assignment, here’s
what you need to know about it:
val game = TrotterGame(5, 40)game: o1.trotter.TrotterGame = a 5-by-5 grid game.horseHeadingres12: o1.Direction = Direction.Right game.horseHeading = Direction.Up
In this example, we create a small world (pictured) for the trotter to horse about in. It has five times five squares, and the horse steps 40 pixels, or one square, at a time.
The variable horseHeading
is of type Direction
. This
type is part of O1Library and
represents directions in the sort of two-dimensional
coordinate system that is familiar to us from class Pos
.
It’s a var
, so we can assign a new value to it. The
following values are useful here: Direction.Up
,
Direction.Down
, Direction.Right
, and Direction.Left
.
In TrotterApp.scala
, you can find a GUI that creates a larger grid. Try running the
app. The horse moves. Pressing the arrow keys, you’ll notice that you can only turn
the horse upwards.
The given GUI already contains the code that produces the graphics as well as an
onTick
method. There’s also a good start for the onKeyDown
event handler, whose
job it is to interpret keys (Key
) as headings of the horse (Direction
). Here is
the code as given:
override def onKeyDown(key: Key) =
if key == Key.Up then
game.horseHeading = Direction.Up
There are two separate concepts called Up
here:
Key.Up
is a constant that corresponds to the upward arrow
key on the keyboard. Direction.Up
, on the other hand, means
the upward direction towards smaller y coordinates.
Your official task is just to complete this method so that it turns the horse also in
the other three main directions (Right
, Down
, Left
). You can do this simply by
adding similar if
commands (even though this makes the code repetitive). If you want,
you can also experiment with other modifications to the given code as you please.
A+ presents the exercise submission form here.
Using the keyboard to accelerate
Package o1.charger
in MoreApps contains a similar application where you can direct a
“charger” in a flat, two-dimensional world. Holding down a key adds to a charger’s speed.
The given version of the program fails to get the charger moving. Fix that:
Flesh out method
onKeyDown
much like you did in the previous assignment: the GUI should react to four arrow keys by calling the charger’saccelerate
method and passing in the appropriateDirection
parameter.Study class
Charger
. Its intended behavior is described in the comments within the code. For the most part, the implementation’s already there, but some parts are missing.Complete the branches of the
if
inaccelerate
so that they adjust the charger’s speed and direction as specified.Complete
move
so that the method changes the charger’s position based on its speed and direction.The given code demonstrates that each
Direction
object has adx
variable that indicates how much the x coordinate changes if an object moves a single unit in that direction. SoDirection.Right
has adx
of +1 andLeft
has adx
of -1, whileUp
andDown
have adx
of zero. Direction objects similarly have ady
attribute.With that information,
move
is simpler to implement. (If you need a hint, you can return to the previous assignment and studyTrotterGame
.)
Test your program.
It would be nice if the charger’s image reflected its direction of movement. This is easily accomplished. We already know that class
Pic
has the methodsclockwise
andcounterclockwise
that rotate an image (Chapter 2.3). Moreover, everyDirection
object has a method namedtoDegrees
that you can use as shown below.Direction.Up.toDegreesres13: Double = 90.0 Direction.Left.toDegreesres14: Double = 180.0
Loosely speaking, what the method does is answer the question: if we’re facing in this direction, how many degrees would we have to turn clockwise in order to face rightwards? For example,
Direction.Up
is 90 degrees counterclockwise fromDirection.Right
.Edit the method
makePic
in the GUI so that it rotates the charger’s image to point towards its heading.
A+ presents the exercise submission form here.
Adopting class Velocity
In the previous assignment, we used two separate variables to represent the charger’s speed (i.e., how much its position changes per step) and direction. Together, speed and direction constitute velocity.
It’s both natural and convenient to represent the concept of velocity as a class. And you don’t have to roll your own: velocity is a useful concept in many applications and there’s a class for it in O1’s library:
val test = Velocity(Direction.Up, 12.3)testVelocity: o1.world.Velocity = Velocity(Direction.Up,12.3) test.directionres15: o1.world.Direction = Direction.Up test.speedres16: Double = 12.3
A velocity consists of a speed and a direction.
The class has many methods, some of which we’ll find useful later and some of which we’ll use right now:
Take a look at the Scaladocs of class
Velocity
. Note in particular the methodsfaster
,noFasterThan
, andnextFrom
.Refactor class
Charger
(that is, rework its code without changing what it does): Replace the separate variablesangle
andspeed
with a single variable of typeVelocity
. Adjust the methodsheading
,accelerate
, andmove
so that they use the new variable.Don’t touch the class’s public interface Each method must still do what it did before.
A+ presents the exercise submission form here.
Using the keyboard to glide smoothly
Our charger program is lacking in many ways. For one thing, the charger’s movements are unpleasantly rigid. Moreover, the way we’ve handled key presses so far is unsatisfactory from a technical point of view:
The runtime environment of the program invokes
onKeyDown
in the event of the user pressing a key. The method is also intermittently reinvoked as long as the key remains down. However, the number of these subsequentonKeyDown
invocations isn’t standard across the different environments where we may run our program. This means that the program’s behavior doesn’t rest on a solid foundation.Also due to the nature of
onKeyDown
events, pressing down multiple keys at once doesn’t work as we might wish: try hitting one arrow key while holding another down, and the application will “forget” the held-down key. This doesn’t feel natural to the user and doesn’t bode well for our future endeavors such as using a combination of two keys for diagonal movement.
Let’s adopt a slightly different approach to tracking held-down keys. This other approach is a touch more complex but makes it much easier to program smooth movements.
Package o1.glider
contains starter code for a program that controls a “glider”.
Familiarize yourself with the GUI in
GliderApp.scala
. Note at least the following.The application bears a clear resemblance to the charger app.
The event handler
onTick
simply commands the glider to glide on each tick.The primary difference compared to the charger app is in the
onKeyDown
method: it doesn’t actually increase the glider’s speed when the up arrow is pressed but simply records that ”the gas pedal is down”.onKeyUp
similarly handles keys being released.
Look at class
Glider
. Note at least the following.A glider has a
Velocity
.It also has three instance variables of type
Boolean
that indicate whether the “gas pedal” is down and whether the “steering wheel” has been turned left or right.The method
glide
is responsible for the glider’s movement. It adjusts the glider’s velocity based on the controls and the glider’s position as per its current velocity.The glider doesn’t have brakes, but “friction” slows it down slightly at the end of each movement step so that the glider eventually comes to rest unless accelerated.
The code that actually changes the glider’s position and applies friction has been given. What you should do is enable the glider to change speed and direction; this goal is detailed in the comment that precedes
glide
.glide
delegates this subtask to the private methodadjustVelocity
. You can make the necessary changes there.
Now you can give GliderApp a spin. The glider should start moving when you press the up arrow and gradually slow down by itself. It doesn’t turn yet.
Fill in the missing code in
GliderApp.scala
’s event handlers so that they inform theGlider
object when it should turn.
A+ presents the exercise submission form here.
Another glider
If you wish, you can try adding another glider to the view. Make it respond to the keys W, A, and D, for instance.
If you do this, and consequently end up with code that works but looks repetitive, you can reflect on why that happened. In future chapters, we’ll come up with more ways to eliminate redundant code.
More keys
What would be an easy way to respond to a Shift-click of the mouse? Or to determine if one of the special keys Shift, Ctrl, or Alt is down while the user pressed another key?
One of the optional assignments of Chapter 3.1 featured an event handler
that received not just the click’s location but an event object of type
MouseClicked
. Other event handlers, too, can be defined so that they receive
a reference to an event object as a parameter. Such an event object can provide
various kinds of information about the GUI event that occurred, such as whether
a particular special key was down at the time. Here are a couple of examples:
override def onClick(mouseEvent: MouseClicked) =
println(mouseEvent.isControlDown)
override def onKeyDown(keyPressEvent: KeyPressed) =
println(keyPressEvent.isAltDown)
Note the types of the parameter variables.
You can try adding those methods to one of the preceding programs. You can also
check out the other event handler described in View
’s documentation. Perhaps you have an idea of your own for a small
app that responds to the special keys?
Further Reading on Randomness
Applications of randomness
Random.org’s introductory article on Randomness already came up above. That’s a good place to start.
Randomness gives me gray hairs since it doesn’t actually exist. Sort of.
Where do you need randomness that’s more random than what a computer can produce?
In online poker, for example.
Large-scale computer simulations, such as those used for predicting weather, may work poorly if the distribution of random numbers is even slightly off.
Pseudo-randomness may compromise information security. If a random-number generator is used for security purposes but is predictable, hackers may exploit it for nefarious purposes.
For more information, see Applications of Randomness on Wikipedia.
Summary of Key Points
Producing a truly random number is anything but trivial.
The Scala API includes tools for producing pseudorandom numbers that are often “random enough”.
You can write private methods that help you implement a class.
Links to the glossary: if, pseudorandom number, random seed; private.
Feedback
Please note that this section must be completed individually. Even if you worked on this chapter with a pair, each of you should submit the form separately.
Credits
Thousands of students have given feedback and so contributed to this ebook’s design. Thank you!
The ebook’s chapters, programming assignments, and weekly bulletins have been written in Finnish and translated into English by Juha Sorva.
The appendices (glossary, Scala reference, FAQ, etc.) are by Juha Sorva unless otherwise specified on the page.
The automatic assessment of the assignments has been developed by: (in alphabetical order) Riku Autio, Nikolas Drosdek, Kaisa Ek, Joonatan Honkamaa, Antti Immonen, Jaakko Kantojärvi, Niklas Kröger, Kalle Laitinen, Teemu Lehtinen, Mikael Lenander, Ilona Ma, Jaakko Nakaza, Strasdosky Otewa, Timi Seppälä, Teemu Sirkiä, Anna Valldeoriola Cardó, and Aleksi Vartiainen.
The illustrations at the top of each chapter, and the similar drawings elsewhere in the ebook, are the work of Christina Lassheikki.
The animations that detail the execution Scala programs have been designed by Juha Sorva and Teemu Sirkiä. Teemu Sirkiä and Riku Autio did the technical implementation, relying on Teemu’s Jsvee and Kelmu toolkits.
The other diagrams and interactive presentations in the ebook are by Juha Sorva.
The O1Library software has been developed by Aleksi Lukkarinen and Juha Sorva. Several of its key components are built upon Aleksi’s SMCL library.
The pedagogy of using O1Library for simple graphical programming (such as Pic
) is
inspired by the textbooks How to Design Programs by Flatt, Felleisen, Findler, and
Krishnamurthi and Picturing Programs by Stephen Bloch.
The course platform A+ was originally created at Aalto’s LeTech research group as a student project. The open-source project is now shepherded by the Computer Science department’s edu-tech team and hosted by the department’s IT services. Markku Riekkinen is the current lead developer; dozens of Aalto students and others have also contributed.
The A+ Courses plugin, which supports A+ and O1 in IntelliJ IDEA, is another open-source project. It has been designed and implemented by various students in collaboration with O1’s teachers.
For O1’s current teaching staff, please see Chapter 1.1.
Additional credits appear at the ends of some chapters.
An object of type
Random
is a “random number generator”. The class is located in packagescala.util
. (That package contains an assortment of miscellaneous tools. Its name is short for “utilities”.)