CastleLab / COMP3021-2021Fall-PA1-Student-Version

HKUST - COMP3021 - 2021Fall - Programming Assignment 1 - Student Version.
0 stars 2 forks source link

COMP3021 PA1: Inertia ASCII Game

Inertia is a puzzle game, originally implemented by Ben Olmstead. The objective of this game is to collect all gems without running into mines, with the added twist that you will not stop once you start moving until you hit either a wall, a "stop cell", a mine, or the border of the game board.

An online demo of the game can be found here.

This is a long file. Please take your time to read this document, and refer to this document often if you have any questions before asking your TAs.

Quick Links

Skeleton Code Updates

Game Elements

There are two types of game elements: Cells and entities.

Cells

Cells are the components that make up a game board. Each cell represents a single tile on the game board.

Cells are categorized into the following types:

Entities

Entities are objects that can reside on an entity cell.

Entities are categorized into the following types:

Game Representations

There are two modes in which you can run this game.

ASCII Version

By default, the game runs in ASCII mode. A simple game board will look as follows:

$ java --enable-preview -jar PA1-obf.jar puzzles/00-unicode-test.game
@#*
LWX
...

Where each character represents the following type of tile:

Unicode Version

If your console supports Unicode characters, you may also opt to play the game in Unicode mode, by passing --unicode to the arguments of the JAR.

$ java --enable-preview -jar PA1-obf.jar --unicode puzzles/00-unicode-test.game
◯□◇
♡█⚠
...

Where each character represents the following type of tile:

You might want to zoom into your console using this mode, since the characters might be difficult to differentiate using the default font size.

Game Mechanics

Basic Controls

Map Specifications

When loading a map, you must ensure the following requirements are met:

These requirements are also stated in the constructor of GameBoard. You should throw an IllegalArgumentException if any of these requirements are not met. See the Exceptions section on how to throw an exception.

Game Rules

See the Game Representations section for how to interpret the game board. Note that this section may not cover all cases; You are encouraged to check the obfuscated JAR for any behavior unspecified in this document.

Undo

The undo functionality undoes a single step made by the player. You should ensure that all game state is restored by the undo logic, including:

You should NOT modify the following during the undo:

For example:

          Move             Undo
@L*.#.M   --->  #...@.M    --->  @L*.#.M  
Lives=2         Lives=3          Lives=2
Moves=0         Moves=1          Moves=1

Winning and Losing Conditions

The game is won when all gems are collected from the game board.

The game is lost when the player runs out of lives. This means that if the player has an unlimited number of lives, the game will never be lost (unless the player quits in frustration).

Hints for "All Gems Reachable" Implementation

Not sure how to implement this algorithm?

In order to determine whether all gems are reachable by the player, you first need to determine all cells which are reachable from the player. Once you have that information, you can then check whether all gems reside in a cell which is reachable from the player.

All-gems-reachable(board, player):
    NumTotalGems := Count-gems(board)
    ReachableCells := All-reachable-cells(player)

    NumReachableGems := 0
    foreach cell in ReachableCells:
        if cell contains Gem:
            NumReachableGems := NumReachableGems + 1

    return NumTotalGems == NumReachableGems

How do you determine which cells are reachable from the player then? The flood fill algorithm, which is a family of algorithms for filling in closed regions, may be helpful. You may refer to the algorithms' pseudocode implementations to understand how this algorithm can be implemented.

Note that you do not need to consider mines as a potential obstacle; Only walls need to be considered. However, you are free to add to this algorithm to provide more robust checking of whether a gem is reachable.

Loading a Game

The game must be launched with an argument specifying the game file to load.

Several puzzles are provided for you under the directory puzzles.

Obfuscated JAR

An obfuscated JAR is provided for reference, located in the artifacts directory.

To launch the game using the obfuscated JAR, use the following command-line:

$ java --enable-preview -jar PA1-obf.jar [--unicode] PUZZLE_PATH

For instance, to load the game puzzles/00-unicode-test.game:

$ java --enable-preview -jar PA1-obf.jar [--unicode] puzzles/00-unicode-test.game

IntelliJ IDEA

When testing your own implementation, you may use the provided Main Run Configuration.

To edit the command-line arguments, go to Run > Edit configurations, and edit the highlighted text box:

Run Configuration

Designing Custom Games

Done with the assignment or just wanting to slack off? Why not design your own games to play with?

The format of each game file is as follows:

<number of rows>
<number of columns>
<number of lives; may be blank for unlimited>
<row 1 cells>...
<row 2 cells>...
...

Note that the representation of these cells are (yet again) different from the ones used for display:

Note that loading a game has been implemented in GameStateSerializer, so you do not need to do anything there.

See the puzzles directory for examples.

Java Concepts

You are expected to be familiar with the lecture content up to Week 3; Other concepts which are not taught up to that point are explained in this section.

You may also want to look at the "optional" section to read about the Java concepts used to implement the provided code.

Exceptions

Lecture Slides

Exceptions are a kind of error-reporting mechanism used in exceptional circumstances (see what I did there?). They are generally used to indicate unexpected conditions or program state while the program is running.

In general, the syntax for throwing an exception is

throw new <className>(<params>...);

where <className> is the exception class (which extends Exception or Throwable) you want to throw.

There are several methods scattered around the skeleton code that requires you to throw an exception when certain conditions are (not) satisfied. When using these methods, remember to observe the requirements as documented by the method's Javadoc (especially the @throws section). For example, for Position.offsetBy:

    /**
     * ...
     * @throws IllegalArgumentException if any component of the resulting coordinate is negative.
     */
    @NotNull
    public Position offsetBy(@NotNull final PositionOffset offset) {
        // ...
    }

This indicates that you should probably check the requirement of the method is met before calling this method, or use some other alternative that does not have this requirement.

You may also catch an exception to "recover" from an error. The general syntax for that is

try {
    // do something that might throw an exception
} catch (<className> <variableName>) {
    // recovery logic
}

Note that this assignment does not and should not require the use of try-catch blocks; In fact, the reference solution does not make use of try-catch blocks (except for the provided code). Use them at your own risk!

Sealed Classes

Lecture Slides

Sealed classes are a special kind of class which only allows a specific classes to inherit from it. The purpose of sealed classes is to limit a class to only contain a known subset of subclasses.

Consider an example:

abstract class Binary {

    public String toString() {
        if (this instanceof Zero) {
            return "0";
        } else if (this instanceof One) {
            return "1";
        } else {
            // What are we supposed to do here???
            throw new RuntimeException();
        }
    }
}
class Zero extends Binary {}
class One extends Binary {}

For Binary.toString(), the last else branch is necessary because there may theoretically be a third class that extends Binary, despite that we know Zero and One are the only possible subclasses.

Sealed classes solve this problem by requiring all subclasses of a sealed class to either be a nested static class or included in the class declaration, for example:

sealed abstract class Binary {

    final static class Zero extends Binary {}
    final static class One extends Binary {}

    public String toString() {
        // ...
    }
}
// or
sealed abstract class Binary permits Zero, One {

  public String toString() {
    // ...
  }
}

final class Zero extends Binary {}
final class One extends Binary {}

The implementation for Binary.toString() still needs to be the same, because Java currently does not have a mechanism to deduce this information. However, in Java 17 (Preview), Pattern Matching for Switch will allow for this:

sealed abstract class Binary {

    public String toString() {
        return switch (this) {
            case Zero zero -> "0";
            case One one -> "1";
            // No default needed! Yay!
        }
    }
}

The Java proposal for Sealed Classes can be found here.

Jetbrains Annotations

Are you tired of seeing NullPointerException? Do you want the IDE to remind you when you accidentally pass null into a method that doesn't want it?

One of the most common bugs in Java is the notorious NullPointerException, which occurs when accessing instance members or fields of a variable holding a null reference. This issue is so common that, in Java 14, a feature was implemented to make NullPointerExceptions emit more information to help Java developers diagnose this class of issues.

Obviously, it is best if we just avoid this altogether and ensure that we are not passing nulls into methods that do not accept them.

Jetbrains Annotations is a library by Jetbrains (the company behind IntelliJ IDEA), providing a set of Java annotations to improve IDE diagnostics. In this assignment, most fields and all methods are marked either as @Nullable or @NotNull, indicating that the values can be null and cannot be null respectively.

If you pass null into a parameter or return value marked as @NotNull, you will get a warning in the IDE:

@NotNull
public static Object foo(@Nullable Object o) {
    bar(o);
    bar(null);
    return null;
}

public static void bar(@NotNull Object o) {}

Null warnings

The use of these annotations is optional. They do not have effects during compile-time.

Tasks

Complete all the TODOs in the entire project. A detailed description of each task is provided in the Javadoc above each method. In IntelliJ IDEA, go to View > Tool Windows > TODO to jump to each TODO in the project. You may replace the placeholder implementation in the methods marked as TODO.

You may also add private methods to classes if it aids your implementation. Adding non-private methods are highly discouraged and may cause point deductions!

TODO Practiced Concepts
model.Cell (and its descendents) Basic Java, Inheritance
model.StopCell Inheritance, Overriding
model.Entity (and its descendents) Basic Java, Inheritance
model.Direction Basic Java
model.Position Records
model.GameBoard Class, Arrays, Java Collections
model.GameState Class
model.MoveResult Sealed Class, Java Collections
model.MoveStack Class, Java Collections
controller.GameBoardController Basic Java, Class
controller.GameController Basic Java, Class

Some unit tests are provided to test your implementation.

Code Style

Since this is a Java course, we expect you to write idiomatic Java with a good code style. As such, we employ the tool CheckStyle to help you check the style of your implemented code.

You may use the CheckStyle Run Configuration to run CheckStyle on your code. The report will be generated in app/build/reports/checkstyle.

Note that a good code style is part of the grading scheme.

IntelliJ Run Configurations

To help you with the different tasks offered by Gradle, we have bundled some run configurations for you in IntelliJ, so that you can just choose what you want to run.

Note that for testing, there is an option for Gradle and an option for JUnit. While usually we would use the Gradle version, a bug in IntelliJ causes the test panel to not show up when testing using Gradle. Therefore, we suggest using the JUnit version of the test configurations instead.

Submission

You should submit a ZIP file containing the following:

You need to submit your ZIP file to CASS. The deadline for this assignment is October 3rd 2021, 23:59.

Grading Scheme

Percentage Notes
Keep your GitHub repository private 5% You must keep your repository private at all times.
Commit at least 3 times in different days 5% You should commit three times during different days in your repository.
Code Style 10% You get 10% by default, and every 5 warnings from CheckStyle deducts 1%.
Provided Tests 15% (# of passing tests / # of provided tests) * 15%
Hidden Tests 65% (# of passing tests / # of hidden tests) * 65%

Note that sanity tests are not part of the marking scheme because they are to make sure the critical part of the skeleton code is working. They will pass without you having to implement anything. You will get zero for the both the provided and hidden test part (counted for 80%) if you break any of the sanity tests, which means you will get at most 20% for this assignment.

For your information, there are:

Plagiarism

We trust that you are familiar with HKUST's Honor Code. If not, refer to this page.

FAQ

Please read the discussions section to see if your question has already been asked/answered before.

Want To Know More?

File I/O

Lecture Slides (Slide 66)

File I/O is mainly used in this assignment for reading the game from a file.

Since the beginning, Java has a set of I/O APIs (residing under java.io) which, as the name suggests, performs I/O operations. For example, java.io.File (which was briefly covered in Week 2's slides) is an abstraction over a single file or directory on a system.

Soon, it was realized that the original set of I/O APIs is not optimally designed, and thus NIO (and NIO2) was created to address these issues. Classes from NIO all reside under the java.nio package. While the original I/O API has not been deprecated, there has been a shift since Java 7 to use the NIO methods instead.

The entire GameStateSerializer class is implemented using NIO methods.

A tutorial on the entire NIO API can be found here.

Note: The process of converting Java objects (or data types in any other programming language) into a String is called serialization. The opposite process (converting a String into a Java object) is called deserialization.

MVC Design Pattern

Why are controllers only allowed to mutate the game state? Why are views only allowed to access a read-only representation of the game state?

If you have learnt about software engineering practices before (COMP3111, anyone?), this design pattern may be familiar.

This design pattern is known as MVC (Model-View-Controller). The core principle of MVC is that for any user interface (UI), there are three components:

While the benefits of MVC are (like most other things) hotly debated, separating components by their responsibilities helps improve maintainability. It is nice to learn MVC because many popular open-source projects are such implemented.

The Wikipedia article can be found here.

Gradle

Recall in COMP2012 (or COMP2012H), you were taught Makefiles. You were taught that Makefiles simplify the compilation process by removing the need to compile individual files on your own, knowing only to recompile dependent targets, etc...

In Java, build systems like GNU Make also exists. In this assignment (and subsequent assignments, as well as labs), we will be using a build system called Gradle.

Like Make, Gradle helps developers manage projects by simplifying the compilation process and caching compilation results. However, Gradle can do a lot more than Make can, for instance:

Also like Make, Gradle projects organizes itself into tasks. For example:

However, you will mostly be interacting with Gradle via IntelliJ IDEA.

Gradle is configured using buildscript files written either in Groovy or Kotlin (both are languages which can run on the Java VM). The corresponding files can be found in settings.gradle.kts and app/build.gradle.kts in this assignment.

You are not expected to learn nor understand how Gradle works in this course. However, if you are interested in developing larger Java projects or Android applications, you may be interested in reading more about Gradle.