This course has already ended.

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 8.1: A Game of Glasses

About This Page

Questions Answered: How will I manage with a bigger program with multiple classes?

Topics: The chapter revolves around a single programming assignment, which features collection methods and subtyping, among other things.

What Will I Do? Study given code and program.

Rough Estimate of Workload:? Two or three hours.

Points Available: B60.

Related Modules: Viinaharava (new).

../_images/person10.png

The Game of Viinaharava

../_images/viinaharava.png

A game of Viinaharava in progress. Some of the glasses have been drunk; those squares either contain a number indicating the number of neighboring booze glasses or are empty to indicate that there is no booze in the immediate vicinity. (Yes, it’s a different-looking Minesweeper.)


The local temperance society has commissioned a game that promotes water as a healthy drink. To that end, a game named Viinaharava has been designed; its implementation is more or less ready but needs you to flesh it out.

Viinaharava takes place on a board that consists of small drinking glasses arranged in a grid. Most of them contain water but a few contain a stiff, transparent alcoholic drink, “a booze”. The player’s task is to drink all the water glasses without touching a booze.

The player virtually drinks a glass by clicking on it. Their task is simplified by a hint at the bottom of each glass: the number of boozes in neighboring glasses. The game is over when either all the water or even a single booze has been drunk.

Task description

You’ll find a partially operational implementation in the Viinaharava module. See below for an introduction.

Study this module and fill in the missing parts. You may wish to follow these steps:

  1. Launch Viinaharava with the app object o1.viinaharava.gui.Viinaharava. The board shows up but the game doesn’t work.

  2. Study the class o1.Grid, which has been used in Viinaharava’s implementation. See below for further information.

  3. Familiarize yourself with the classes in package o1.viinaharava. Start from the overview of the package below, then turn to the Scaladocs and the source code.

  4. Once you understand the program as given, add the missing parts. See below for additional instructions and hints.

Representing Dense Grids

You’ll remember Snake from Chapter 6.3. In that game, the snake and its food were located on the spaces of a grid-like playing field, which we recorded as GridPos objects. Each GridPos was composed of two integers x and y, the pair of which pinpointed a space on the grid.

Viinaharava resembles Snake: it, too, has a playing field that is essentially a grid. We can again use GridPos as we represent the locations of each glass on the game board.

(In this assignment, you don’t have to concern yourself with pixels or graphics. The given GUI takes care of that. You can focus on modeling the rules of the game itself. You just need to consider each glass’s position on the grid, its GridPos.)

In Snake, we had a “sparse” grid: there were few actual items (snake segments; food) in the grid compared to the total number of spaces. We represented the game’s state by simply tracking those GridPos coordinates that actually did contain something and considered each other space to be empty,

This time we’ll be different and represent game boards as “dense” grids. We’ll record, for every single space on the board, which kind of glass it contains: Is it a water glass or a booze? Have the contents been drunk already? How many dangerous neighbors does it have?

We’ll find it easier to represent dense grids if we adopt a tool designed for just that purpose.

The Grid trait

The o1 package provides a Grid trait. Each Grid objects represents a grid that consists of elements of similar size that have been laid out in rows and columns. The elements could be glasses, for example.

The trait has a number of methods for manipulating such grids. For instance, there are methods for picking out a particular element given its position (elementAt and apply), finding all the spaces that are adjacent to a given space (neighbors), and determining the grid’s dimensions (width, height, and size).

Since Grid is a trait, we can’t simply say Grid(...) to create an instance; we instantiate it via subtypes. The Grid trait is designed to work in different applications that feature grids and GridPoses: it doesn’t specify what kind of spaces grids consist of. That’s something we’ll need to specify in a subtype.

Viinaharava is a particular use case for Grid: each game board is a grid that consists of glasses. (In later assignments, we’ll use Grid to represent grids with other kinds of content.)

The Viinaharava Module

Module Viinaharava contains two packages. We won’t go into the GUI package o1.viinaharava.gui and you don’t need to understand how it works; it’s enough that you find the app object there and use it to start the program. The parent package o1.viinaharava, however, is very topical. Its key classes are these two:

  • Glass: instances of this class represent individual glasses that the game board consists of.

  • GameBoard: a GameBoard object represents an entire game board, a grid of Glasses. GameBoard is a subtype of Grid:

The diagram below describes the relationships between the classes:

../_images/module_viinaharava.png

The lower part of the diagram means that each game board is associated with multiple glasses, each at its particular position: we can use a GridPos to pick out a particular Glass on a GameBoard.

Glass and its missing methods

Each glass can be either full or empty. It can be either a glass of water or a glass of booze. Moreover, each Glass object keeps track of how dangerous it is: how many boozes there are in the adjacent glasses. The danger level is a number between zero and eight; diagonally adjacent counts, too.

Glass objects have instance variables for recording their contents and danger level. Each glass also “knows” which game board it’s on and which GridPos it’s at.

When created, a glass is full of water. The Glass class has methods for modifying that initial state. Specifically:

  • We can empty a glass. The empty method is invoked whenever the user (left-)clicks a glass in the GUI.

  • We should be able to fill a glass with booze (pourBooze). This has the additional effect of increasing the danger levels of neighboring glasses. pourBooze is called several times at the start of each game to place the hidden booze on the board. (For testing purposes, the GUI also lets the player add booze during a game.)

pourBooze lacks an implementation, though. The neighbors method, which is supposed to find the adjacent glasses, is also missing.

GameBoard and its missing methods

Here’s a start for the GameBoard class:

class GameBoard(width: Int, height: Int, boozeCount: Int) extends Grid(width, height):
  // ...

A new GameBoard instance needs three constructor parameters: the number of columns on the grid, the number of rows, and the number of booze glasses initially hidden on the board.

Initializing any Grid object requires a width and a height. We pass these two parameters onwards to the trait.

The class header needs one more thing before it works. This is because the supertype Grid demands a type parameter in addition to the constructor parameters. Just like we have used square brackets to mark the element type of a Buffer, we can mark the element type of a Grid:

class GameBoard(width: Int, height: Int, boozeCount: Int) extends Grid[Glass](width, height):
  // ...

A GameBoard object is a Grid whose elements are Glass objects.

As you saw when you launched the game, the given implementation already fills the board with water glasses. A further inspection of the given code in GameBoard.scala shows us how:

class GameBoard(width: Int, height: Int, boozeCount: Int) extends Grid[Glass](width, height):

  def initialElements =
    val allLocations = (0 until this.size).map( n => GridPos(n % this.width, n / this.width) )
    allLocations.map( loc => Glass(this, loc) )

  this.placeBoozeAtRandom(boozeCount)

  // ...
end GameBoard

As the documentation says: this method, which produces a collection of all the elements that initially occupy the grid, is left as abstract by the Grid trait. (However, the trait automatically calls this method when a new grid is created.)

The subtype GameBoard implements the method by returning a collection of empty Glasses. Feel free to study this implementation, but it’s not strictly necessary for the present assignment. Don’t change this method.

The placeBoozeAtRandom call written directly into the class body is part of the code that initializes new instances of GameBoard (i.e., the class’s constructor). The method is invoked every time a new GameBoard is created.

The aforementioned placeBoozeAtRandom method doesn’t have an implementation yet, so there’s no booze on the board. That will require your attention.

The drink method is also missing, which is why the game doesn’t do anything when clicked. So are isOutOfWater and isGameOver, which are related to ending the game.

You may tackle with the assignment in four steps as described below.

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.

a drop of ink
Posting submission...