Lab 11: BYOW Introduction

FAQ #

Each assignment will have an FAQ linked at the top. You can also access it by adding “/faq” to the end of the URL. The FAQ for Lab 11 is located here.

Introduction #

This lab will help you with Project 3: Build Your Own World (BYOW). The first part will teach you how to use a set of “tiles” to generate shapes on your screen. This will apply to building the rooms, hallways, and other features of your world in Project 3. Next week’s lab will go more into implementing interactivity, which is relevant towards building a part of Project 3 (but more on this in the next lab).

We’ve also included an optional, but highly recommended part of this lab that touches on how to read and write into a file, which will come in handy for a later portion of Project 3 (saving and loading).

Pre-Lab #

Some steps to complete before getting started on this lab:

  • As usual, use git pull skeleton main

  • Watch a previous semester’s project 3 getting started video at this link.
  • Note the name and API have changed slightly, but the bigger picture still applies.

  • Understand that project 3 will be a marathon and not a sprint. Don’t wait until the last minute. You and your partner should start thinking about your design NOW.

  • Read over Phase 1 of the project 3 spec.

In the first half of this lab, you and your partner will learn some basic techniques and tools that will be helpful for project 3.

Part I: Meet the Tile Rendering Engine #

Boring World #

Open up the skeleton and check out the BoringWorldDemo file. Try running it and you should see a window appear that looks like the following:

boring world

This world consists of empty space, except for the rectangular block near the bottom middle. The code to generate this world consists of three main parts:

  • Initializing the tile rendering engine.
  • Generating a two dimensional TETile[][] array.
  • Using the tile rendering engine to display the TETile[][] array.

The API for the tile rendering engine is simple. After creating a TERenderer object, you need to call the initialize method, specifying the width and height of your world, where the width and height are given in terms of the number of tiles. Each tile is 16 pixels by 16 pixels, so for example, if we called ter.initialize(10, 20), we’d end up with a world that is 10 tiles wide and 20 tiles tall, or equivalently 160 pixels wide and 320 pixels tall. For this lab, you don’t need to think about pixels, though you’ll eventually need to when you start building the user interface for Project 3.

TETile objects are also quite simple. You can either build them from scratch using the TETile constructor (see TETile.java), or you can choose from a palette of pregenerated tiles in the file Tileset.java. For example, the code from BoringWorldDemo.java below generates a 2D array of tiles and fills them with the pregenerated tile given by Tileset.NOTHING.

TETile[][] world = new TETile[WIDTH][HEIGHT];
for (int x = 0; x < WIDTH; x++) {
    for (int y = 0; y < HEIGHT; y++) {
        world[x][y] = Tileset.NOTHING;
    }
}

Of course, we can overwrite existing tiles. For example, the code below from BoringWorld.java creates a 14 x 4 tile region made up of the pregenerated tile Tileset.WALL and writes it over some of the NOTHING tiles created by the loop code shown immediately above.

for (int x = 20; x < 35; x++) {
    for (int y = 5; y < 10; y++) {
        world[x][y] = Tileset.WALL;
    }
}

INFO: Note that $(0, 0)$ is the bottom-left corner of the world in this case (not the top-left as you may be used to).

The last step in rendering is to call ter.renderFrame(world), where ter is a TERenderer object. Changes made to the tiles array will not appear on the screen until you call the renderFrame method.

Try changing the tile specified to something else in the Tileset class other than WALL and see what happens. Also experiment with changing the constants in the loop and see how the world changes.

NOTE: Tiles themselves are immutable! You cannot do something like world[x][y].character = 'X'.

INFO: Why do we initialize the world to Tileset.NOTHING, rather than just leaving it untouched? The reason is that the renderFrame method will not draw any tiles that are null. If you don’t initialize the world to Tileset.NOTHING, you’ll get a NullPointerException when you try to call renderFrame.

Random World #

Now open up RandomWorldDemo.java. Try running it and you should see something like this:

random world

This world is sheer chaos – walls and flowers everywhere! If you look at the RandomWorldDemo.java file, you’ll see that we’re doing a few new things:

  • We create and use an object of type Random that is a “pseudorandom number generator”.
  • We use a new type of conditional called a switch statement.
  • We have delegated work to functions instead of doing everything in main.

A random number generator does exactly what its name suggests, it produces an infinite stream of numbers that appear to be randomly ordered. The Random class provides the ability to produce pseudorandom numbers for us in Java. For example, the following code generates and prints 3 random integers:

Random r = new Random(1000);
System.out.println(r.nextInt());
System.out.println(r.nextInt());
System.out.println(r.nextInt());

We call Random a pseudorandom number generator because it isn’t truly random. Underneath the hood, it uses cool math to take the previously generated number and calculate the next number. We won’t go into the details of this math, but see Wikipedia if you’re curious. More importantly, the sequence generated is deterministic, and the way we get different sequences is by choosing what is called a “seed”.

In the above code snippet, the seed is the input to the Random constructor, so 1000 in this case. Having control over the seed is pretty useful since it allows us to indirectly control the output of the random number generator. If we provide the same seed to the constructor, we will get the same sequence values. For example, the code below prints 4 random numbers, then prints the SAME 4 random numbers again. Since the seed is different than the previous code snippet, the 4 numbers will likely be different than the 3 numbers printed above. This is super helpful in Project 3, as it will give us deterministic randomness: your worlds look totally random, but you can recreate them consistently for debugging (and grading) purposes.

Random r = new Random(82731);
System.out.println(r.nextInt());
System.out.println(r.nextInt());
System.out.println(r.nextInt());
System.out.println(r.nextInt());
r = new Random(82731);
System.out.println(r.nextInt());
System.out.println(r.nextInt());
System.out.println(r.nextInt());
System.out.println(r.nextInt());

In the case a seed is not provided by the user/programmer, i.e. Random r = new Random(), random number generators select a seed using some value that changes frequently and produces a lot of unique values, such as the current time and date. Seeds can be generated in all sorts of other stranger ways, such as using a wall full of lava lamps.

For now, RandomWorldDemo uses a hard coded seed, namely 2873123, so it will always generate the exact same random world. You can change the seed if you want to see other random worlds, though given how chaotic the world is, it probably won’t be very interesting.

The final and most important thing is that rather than doing everything in main, our code delegates work to functions with clearly defined behavior. This is critically important for your project 3 experience! You’re going to want to constantly identify small subtasks that can be solved with clearly defined methods. Furthermore, your methods should form a hierarchy of abstractions! We’ll see how this can be useful in the final part of this lab.

Part II: Use the Tile Rendering Engine #

Knight World Intro #

If you’re unfamiliar with the knight from chess, it moves in an “L”-shape, two squares in one direction and one square in a perpendicular direction. For example, the knight can move from the center square to any of the squares marked with an “X” in the diagram below.

knight moves

We’ve seen how we can draw a world and generate randomness. Your task for this lab is to use the tile generator we’ve seen to draw a world like the one below, where each hole is a knight’s move away from the closest neighboring holes. Note that we’ve only included every other square instead of all eight to create a pleasing repeating pattern. This has been annotated on the above image, specifically with the red squares and has also been annotated on the image below (the blue square is where the knight would be, with the red squares as the corresponding moves from that position).

For the example below, we’ve used Tileset.NOTHING to represent holes and a grey version of Tileset.LOCKED_DOOR to represent floor tiles, but you can use any tilesets you’d like.

The location of the specific holes is flexible, as long as the hole pattern is correct (e.g. translating each of the holes in the image below one square to the left is also valid).

knight world

You should be able to draw differently-sized knight worlds. The picture above contains size-4 holes; below are worlds consisting of size-1, size-2, and size-3 holes, respectively.

knight world size 1

knight world size 2

knight world size 3

In the actual Project 3, you’ll be generating random worlds with rooms and hallways. While this lab task does not directly apply to the project, it will familiarize you with the tile rendering engine and also help you think about how you can take complex drawing tasks and break them into simpler pieces.

Drawing a Knight World #

There are many possible ways to accomplish this task. We’ve provided an extremely basic skeleton with an unimplemented constructor and main method for you to fill in. You can find this skeleton in KnightWorld.java. You should be able to run this file and see a blank world.

If you run this file without any changes, you’ll run into a NullPointerException (refer here to see why).

We’ve included the tiles instance variable and a getTiles method to render your world. Feel free to modify the skeleton as you deem fit.

Managing Complexity #

  • You should absolutely not do everything in a nested for loop with no helper methods. While it is technically possible to do this, you will melt your brain. Without hierarchical abstraction, your mind will transform into a pile of goo under the weight of all the complexity.

  • The DRY (Don’t-Repeat-Yourself) and encapsulation principles are ubiquitous in software engineering. If you find yourself repeating something over and over again, you should probably treat it as its own thing.

  • Don’t hardcode! Wherever possible, you should use variables, methods, and/or classes to represent a larger concept whenever things get unwieldy or there is some arbitrary choice being made. Look for patterns in the given example images above to help you identify what you should be abstracting.

  • If you find yourself repeatedly trying an approach that isn’t working, don’t be afraid to completely scrap your code and try something else (but commit your work first!). You can always restore back to a previous commit.

If you’re stuck, here are some hints. As usual, please try to solve it on your own first.

Hint 1

What depends on the holeSize given? What doesn’t?

Hint 2

Are holes really that different from floors? Is there a way to draw them both without introducing too much complexity?

Hint 3

It will probably be helpful to have something that allows you to draw a square of a specified size somewhere in the world. How can you use this to draw a hole or floor tile?

Hint 4

Holes repeat every five squares (relative to the hole size). Can you extrapolate this pattern to the entire world?

Hint 5

This hint gives away a lot and promotes a certain approach, so only read it if you’re legitimately stuck. You’ve been warned!

To decipher the hint, translate the following hexadecimal numbers to ASCII, twice. (You should probably Google a hex-to-ASCII converter.)

34 43 20 36 46 20 36 46 20 36 42 20 32 30 20 36 31 20 37 34 20 32 30 20 36 31
20 36 45 20 37 39 20 32 30 20 36 37 20 36 39 20 37 36 20 36 35 20 36 45 20 32
30 20 33 35 20 37 38 20 33 35 20 32 30 20 36 32 20 36 43 20 36 46 20 36 33 20
36 42 20 32 30 20 36 46 20 36 36 20 32 30 20 37 33 20 37 31 20 37 35 20 36 31
20 37 32 20 36 35 20 37 33 20 32 30 20 32 38 20 37 32 20 36 35 20 36 43 20 36
31 20 37 34 20 36 39 20 37 36 20 36 35 20 32 30 20 37 34 20 36 46 20 32 30 20
37 34 20 36 38 20 36 35 20 32 30 20 36 38 20 36 46 20 36 43 20 36 35 20 32 30
20 37 33 20 36 39 20 37 41 20 36 35 20 32 39 20 32 30 20 36 39 20 36 45 20 32
30 20 37 34 20 36 38 20 36 35 20 32 30 20 36 35 20 37 38 20 36 31 20 36 44 20
37 30 20 36 43 20 36 35 20 32 30 20 36 39 20 36 44 20 36 31 20 36 37 20 36 35
20 37 33 20 32 45 20 32 30 20 34 34 20 36 46 20 32 30 20 37 39 20 36 46 20 37
35 20 32 30 20 36 45 20 36 46 20 37 34 20 36 39 20 36 33 20 36 35 20 32 30 20
36 31 20 36 45 20 37 39 20 37 34 20 36 38 20 36 39 20 36 45 20 36 37 20 32 30
20 36 39 20 36 45 20 37 34 20 36 35 20 37 32 20 36 35 20 37 33 20 37 34 20 36
39 20 36 45 20 36 37 20 33 46

Moving on to Project 3 #

In theory, this lab has taught you everything you need to know for Project 3! The process of generating your world will be similar in many ways to drawing a knight world, though Project 3 world generation will be considerably more complex. Read over Phase 1 of the project 3 spec.

Take a look at the questions in project3prep.md. Feel free to discuss with your partner or a TA before jotting down your answers.

Checkoff #

Once you have finished KnightWorld and have answered the questions in project3prep.md, you can check off with a staff member in lab. You will be asked to show your world, change the world dimensions, change the hole size, and answer some questions about your code and project3prep.md file. If your work is satisfactory, they’ll give you a magic word to put into magic_word.txt and you’ll be done!

If you’re not able to make it to lab this week for a checkoff, please attend an OH block or make a private post on Ed and fill out the checkoff template. While we’ll try to get back to you as soon as we can, please do not expect us to respond immediately as we’ll process the checkoffs in order, so make sure not to do this last minute!

Persistence (Ungraded) #

In Project 3, you’ll have to implement the ability to save and load your game state, as mentioned in this part of the project spec. While this part of the lab is not graded or required, we highly recommend that you do start thinking early about you might want to do implement this feature. Note that we haven’t talked too much about interactivity in this lab, so the goal of this portion is for you to get familiar with the idea of persistence and see how that might apply once you get to the later part of the project.

Whenever a Java program is run, we use variables to keep track of our values. But once that program ends, those values “no longer exist” or they are no longer accessible. For us to continue accessing those values, we want to ensure that the state of our program persists. This is called persistence.

We’ve provided a class to help you save and load information into a file, called FileUtils. For this exercise, we want to keep track of the number of times the file is written to (excluding the first time, more details in the file). Go ahead and open up SaveLoad and refer to the comments in the file and play around with the given FileUtils class.

Submission #

You’ll be submitting your completed project3prep.md file and magic_word.txt to Gradescope. You will get full credit as long as these are filled out and submitted! The lab is worth 5 points.

Last built: 2024-01-01 04:08 UTC