Multiple Git repository status awareness & grading app – updates

In the post last week I mentioned that I’ve added new features to the tool I use in Data Structures and Algorithms course to supervise, support, test and grade the student programming course work. I’ll describe here the new features and provide some (hopefully) interesting implementation details about those.

Background about the course that helps in understand the role of this tool:

  • students each have their own github or gitlab repository to where they fork the initial status of their course work from the teacher repository,
  • repository contains the task instructions, skeleton code to implement, unit tests code and other supporting code given ready for students,
  • repository tests enable testing the student’s implementation of the data structures and algorithms that are required to implement in the course,
  • teachers have access to the student repositories to assist students with any issues, and finally to grade the work.

Before going into details, a comment about the motivation for this tool to exist in the first place: The course the tool is used in, has 300+ enrolled students and only two full time teachers (sometimes we do have 1-2 assistants with small number of hours, like 50 hrs in total for the whole Autumn), simultaneously teaching 2-3 other 200-300 student courses. That totals around 900+ active students taught simultaneously, so providing individual support and attention to all is practically impossible, at least if no automation (and other support tools) is taken into use.

So the critical thing in managing all this work is, that we do need automated tools to:

  • keep track of how the students are progressing in the course and help those who desperately need it, and after the last deadline,
  • to evaluate and grade the students’ programming submissions as fast as possible.

The grading should be done in three weeks after the last deadline, so it cannot be done in time manually. The aim is to use this tool during the course, trying to catch those students that are not doing OK, and then try to contact and help them in getting their coursework going and spotting any possible mistakes in their work so that they can correct their mistakes before the final deadline.

Students next Fall will receive the same unit tests and the same code analysis tools (in Java unit tests) they can also themselves use. However, this is not enough, especially if the students do not contact the teachers about their issues soon enough or at all. A problem that is too common, unfortunately.

The new features added to the teachers’ tool during this Winter and Spring are:

  1. Automatic source code analysis:
    • results of the source code analysis are stored in a database table; repository table has a column where problems found in code are flagged,
    • tool checks for java imports that are not allowed, notifies about this and sets error flag in the database table,
    • tool also uses the Antrl parser generator to check if method implementations in the source code violate any rules and if the code does not do the things it should do.
  2. Remote repo checks for both github and gitlab:
    • checks that remote is not public, the rule of the course,
    • checks that remote has only allowed contributors.
  3. Testing and grading enhancements
    • tests are now grouped to grade level tests,
    • when all tests in a grade group passes, grade is stored to repository table,
    • grade tests can be executed in whichever order, since grade value is bit flag,
    • test for a certain student repository can be disabled if they cause issues like tests never ending or taking way too much time,
    • if git pull updates code, grade is set to zero and test results are cleared.
  4. Status reporting
    • the tool now generates a textual status report that can be sent to student by email, listing how test pass, if any forbidden things are used / things to use are not used, git statistics etc.

Let’s take a closer look at the code analysis done by the tool in this post. The other features are left for future possible posts to discuss.

Analyzing if the source code uses “forbidden” imports was implemented with simple text parsing techniques. The tool goes through the Java source files in a certain directory where student code is located, reads the .java files and checks if the source uses imports that are not allowed.

For example, when implementing a Stack data structure, student must not import the java.util.Stack and just use it instead of implementing their own stack from scratch, using an array as internal data structure. Or when sorting an array, java.util.Arrays is not imported and Arrays.sort is not used, because student should have used their own implementation of the sorting algorithms.

These kinds of mistakes can be found by analyzing the imports in the student’s .java files. You cannot call Arrays.sort without having import java.util.Arrays in the .java file.

The tool first reads a rules file that contains the import rules for the course:

[allowedImports]
*,import java.util.Comparator
*,import java.util.function.Predicate
*,import oy.interact.tira.
*,import java.lang.Math;
*,import java.util.concurrent.atomic.AtomicInteger;
CodeWordsCounter,import java.io.
CodeWordsCounter,import java.nio.
SetImplementation,import java.util.Iterator

For example, the java.util.Comparator may be used by the students in all of their .java files (*). The same goes with java.util.function.Predicate and all the files in package oy.interact.tira (the course source packages). The class SetImplementation may also import java.util.Iterator.

Et cetera. As you can see, this is an allow list, not a deny list. The imports that are allowed and can be used in the course are small in number, so it is easier to have an allow list than deny list.

Reading the allow list and analyzing the source files was straightforward to implement by simple file handling and text parsing. First read in the rules file and parse the import rules from there, into this structure:

fileprivate
struct SourceRequirementsSpecification {
   var allowedImports: [String: [String]] = [:]

The Swift dictionary object allowedImports has the class as the key (either * or the name of the class rule is about), and then the array of allowed imports the class may contain.

Then the tool starts reading the student .java files and handles all lines beginning with import to check if that class may import that specific thing or not. If the .java file imports something that was not allowed, the tool database table (for the student’s repository) having a column with integer value, a specific bit in that number (a “flag”) is set to 1 (true). This indicates that the student’s code imported something it was supposed to not do.

This is the data structure keeping track of these repository flags, if they are set or not set:

struct Flags: OptionSet {
   typealias RawValue = Int16
   var rawValue: Int16

   static let remoteFails = Flags(rawValue: 1 << 0)
   static let buildFails = Flags(rawValue: 1 << 1)
   static let usesForbiddenFeatures = Flags(rawValue: 1 << 2)
   static let repositoryIsPublic = Flags(rawValue: 1 << 3)
   static let repositoryHasOutsideCollaborators = Flags(rawValue: 1 << 4)
   static let repositoryTestingIsOnHold = Flags(rawValue: 1 << 5)	
}

The Swift OptionSet is a type that presents a mathematical set interface to a bit set. So each flag is a single bit in the 16 bit integer, being either off (0) or on (1). So instead of having six boolean columns in the database table, I can have one 16 bit integer column to store 16 different flags. Most of those still available for future needs.

The flag usesForbiddenFeatures is set in this case of student code using forbidden imports (or other features, discussed below). Other flags are used to:

  • track if the remote repository cannot be accessed at all (remoteFails) or is empty,
  • compiling the code fails (buildFails),
  • repository is public even though it must be private (repositoryIsPublic) and finally
  • if the repository has other collaborators than the student owning the repo and the course teachers (repositoryHasOutsideCollaborators).

These are all issues that have been met in the course during the previous years.

Final flag repositoryTestingIsOnHold can be set manually from the tool user interface for those repositories that mess up the testing, either because the tests are stuck or take too much time and therefore should not be executed before fixed.

That flag struct is a 16 bit integer, the datatype of the database column storing the flags in each student repository database table. If the code has issues related to these rules, the flag is set. When the git repository is updated from the remote, the user needs to run the code analysis again. If a violation of a rule was fixed, the flag is then reset to zero (false), if no other violations are found.

The imports checking from source code is not enough, though.

Sometimes students do mistakes in method implementations they are not supposed to do. Like when implementing a recursive binary search, they implement an iterative binary search. Or when sorting a table in descending order, they first sort in ascending order and then reverse the table order. Since time efficiency is the major topic of the course, this is not what they are supposed to do. Instead, they should use a comparator to sort the table to descending order in the first place, and then no reverse operation is needed at all, making the operation faster.

For checking these kinds of issues the import rules are not enough since these kind of mistakes can be made without importing something. Therefore, the same rules file contains also rules like this:

[codeRules]
# Algorithms.binarySearchRecursive must contain word binarySearchRecursive
Algorithms.binarySearchRecursive,must,binarySearchRecursive
# Algorithms.binarySearchRecursive must not contain word while
Algorithms.binarySearchRecursive,mustnot,while
Algorithms.binarySearchIterative,must,while
CodeWordsCounter.topCodeWords,must,Algorithms.fastSort
CodeWordsCounter.topCodeWords,mustnot,Algorithms.reverse

Here lines beginning with # are comments and ignored by the tool. Illustrating the examples of mistakes above, the rule:

Algorithms.binarySearchRecursive,mustnot,while

specifies that the algorithm binarySearchRecursive in class Algorithms must not contain the keyword while. Since breaking this rule implies that the student has implemented the recursive algorithm using iterative approach. Therefore, the learning goal of implementing a recursive binary search has not been met, and code needs to be fixed.

These rules are also parsed from the rules file into the above mentioned rules structure, which now looks like this:

enum Rule {
   case must
   case mustNot
}

struct CodeRule {
   let methodName: String
   let rule: Rule
   let stringToMatch: String
}

fileprivate
struct SourceRequirementsSpecification {
   var allowedImports: [String: [String]] = [:]
   var rules: [String: [CodeRule]] = [:] // ClassName : CodeRules
}

In addition to the allowedImports rules, the structure now contains also rules for a class (the Swift dictionary key String has the class name), where the CodeRule structure has the name of the method the rule is about, then if the method must or must not (Rule) contain the string to match.

This is a very simple current implementation, and as I develop this further, it may change to something different later. For example, it should be possible to say that the recursive binary search algorithm must not contain “while” but also not the “for” keyword.

The second example about being able to realize that descending order can be implemented by a specific comparator (instead of ascending sort and then doing reverse, which is more time-consuming operation) is checked by the rule that the class CodeWordsCounter in the algorithm topCodeWords (where the work in the coding task is done) must not call Algorithms.reverse.

If a student uses the Java Collections.reverse or Apache Commons ArrayUtils.reverse for this, that mistake is already caught using the import rules.

For this feature implementation, I used the Antrl parser generator. Antlr has existing grammars for many languages, including Java, and it supports generating parsers also for Swift. So all I needed to do was to get the Antlr tool and the Java grammar files (Java20Lexer.g4 and Java20Parser.g4) and then generate the Java parser and lexer for Swift:

$ antlr -Dlanguage=Swift -message-format gnu -o Autogen Java20Lexer.g4
$ antlr -Dlanguage=Swift -message-format gnu -o Autogen Java20Parser.g4

Those commands place the generated Swift source code files in a subdirectory named Autogen.

Using the generated Swift code from Autogen, the tool can then parse Java code. All I needed was to implement a class that extends the Antlr generated Java20ParserBaseListener, give the rules to this class and start parsing the source string read from the student’s java file, containing the Java source code:

import Antlr4
class Java20MethodBodyChecker: Java20ParserBaseListener {

   private let file: String
   private let rules: [CodeRule]
   private var currentMethod: String = ""
   private var stream: ANTLRInputStream? = nil

   var violations: [String] = []
	
   func start(source: String) throws {
      stream = ANTLRInputStream(source)
      if let stream {
         let lexer = Java20Lexer(stream)
         let parser = try Java20Parser(CommonTokenStream(lexer))
         let parserTree = try parser.compilationUnit()
         let walker = ParseTreeWalker()
         try walker.walk(self, parserTree)
      }
   }
// ...

Analysing the code happens in the overridden handler for parsing Java methods, enterMethodDeclaration. There, the implementation checks, using the rules, if the Java code contains any keywords that should not be there or does not contain keywords that should be here:

override func enterMethodDeclaration(_ ctx: Java20Parser.MethodDeclarationContext) {
 if let identifier = ctx.methodHeader()?.methodDeclarator()?.identifier() {
  currentMethod = identifier.getText()
  let ruleSet = rules.filter( { $0.methodName == currentMethod } )
  if !ruleSet.isEmpty {
   if let body = ctx.methodBody() {
    let bodyText = body.getText()
    for rule in ruleSet {
     switch rule.rule {
      case .must:
       if !bodyText.contains(rule.stringToMatch) {
        violations.append("\(file).\(rule.methodName) ei käytä \(rule.stringToMatch), vaikka pitäisi")
       }
      case .mustNot:
       if bodyText.contains(rule.stringToMatch) {
        violations.append("\(file).\(rule.methodName) käyttää \(rule.stringToMatch),  vaikka ei pitäisi")
       }
     }
    }
   }
  }
 }
}

The violations then contains all the things found to be against the rules.

This worked, except for one thing: if the code has comments that do/do not have the specified keywords of the rules, the code analysis did check those comments also. Even though the comments do not influence the implementation and are therefore totally irrelevant.

So if the comments of recursive binary search algorithm contains the keyword while, the tool would report that as a violation of the rule! Or if the code contains a required keyword, but not in the actual code implementation but only in the comments, the analysis would be satisfied. End result: not satisfactory at all.

First I started to fix this by implementing code myself to remove the comments from the code in a String variable before the analysis, but soon I realized that the Antlr parsing tool should somehow already handle this.

What I did instead, was (after some searching on the internet) to modify the Antrl lexer file for Java so that it does not parse the comments at all, but skips them:

// AJU: COMMENT: '/*' .*? '*/' -> channel(HIDDEN);
COMMENT : '/*' .*? '*/' -> skip;

// AJU: LINE_COMMENT: '//' ~[\r\n]* -> channel(HIDDEN);
LINE_COMMENT : '//' ~[\r\n]* -> skip;

Then I regenerated the Swift parser and lexer source files and added them to the tool project.

Using the new lexer/parsers, the parsed Java code does not anymore include comments and I do not have to consider them at all! This works perfectly.

Without asking anything about any “AI” tools out there. As usual, I do not touch them even with a long stick. I prefer to discover and learn myself, as long as the internet is not polluted with “AI” generated crap which makes everything impossible.

Below you can see the tool reporting about imports in a student repository, imports that should not be in the code, and that the code in CodeWordsCounter.topCodeWords does not call Algorithms.fastSort, even though it should, based on the rules in the rules file.

As you can see, the first violation is because the student has misnamed the class as setImplementation. In Java, classes (and therefore also class files) should always begin with capital letters and the name should be SetImplementation instead. So this is a mistake, but actually does not violate the import rule; the Set data structure implementation may use the Iterator. Therefore, when the rules are found to be violated, it often requires human rechecking to make sure no unfair judgments are made by the automation.

Considering the 300 something students in the course, perhaps each of them making at least 3-5 mistakes the code analysis could find, this results in 900-1500 mistakes in total. For the teacher to comb through all the hundreds of lines of code in each of the student projects, repeatedly during the course, working on this “code quality control” task manually is very time consuming and therefore not a realistic task. Especially considering the available teaching resources mentioned in the beginning.

Hopefully the new features of this tool and the similar tests given to students in Java test code, improve the support the teachers can provide during the course to lead the students to the correct path of implementing the required data structures and algorithms.

The post is getting quite long, so the rest of the new features will perhaps be discussed in future posts. Unless I decide to focus on other more interesting topics. Have a nice day!

Course deadlines app

Courses have deadlines. Oftentimes it happens that the deadline comes and the student work is not ready. Even though the deadlines are clearly communicated, often in several places in course materials and website. There may be many reasons for missing a deadline, but whatever the reason, missing one usually leads in to problems in passing the course.

Missing a deadline also causes extra work. Questions are asked, replies sent, and if there is a really good or valid reason, there may be an exception to the rule and extension to the deadline is granted. But if this happens often, in a course of hundreds of students, all this is extra work burdening and stressing both the teachers and the students.

If everybody gets an extension to the deadline, time after time, the concept of deadline becomes irrelevant. If one student gets an extension to the deadline, all others with the same reasons should also get it, since students must be treated equally. Therefore, it would be really, really nice if everybody would acknowledge the deadlines and schedule their work so that no deadlines are missed. A useful habit or skill to have also later in working life…

To make sure a deadlines are not missed since “I forgot it” or “It came sooner than I expected”, I started to remind students of one course about the forthcoming deadlines on my weekly Monday lecture. Since apps are cool, and I like to tinker with things, I wrote an app for this. Obviously, I could have just shown the list of deadlines from the course materials, slideshow or website, but that is boring. Also the app is yet another example of how to implement things, a demo that I can use in various programming courses and a topic for a blog post 🤓

Presenting the Course Deadline Counter app:

Course deadlines app with a list of courses on the left. For the selected course, list of deadlines on the right. Deadlines have symbols and colors informing about the type and urgency of the deadline.
Course deadlines app with a list of courses on the left. For the selected course, list of deadlines on the right. For more screenshots, click the GitHub link in the text and view them from the readme documentation.

The app is localized to English and Finnish, above you can see the Finnish localization screenshot, in GitHub readme, you can see the English locale version screenshots.

Each deadline has graphics and formatting to convey something about the deadline:

  • A symbol (trying) to convey the type of the deadline, an exam (notepad) or a programming task (hammer).
  • Deadlines that are “strict” (dealbreakers; will lead to failing the course if missed, others may be more flexible) are shown with an orange warning sign.
  • Deadlines already passed are shown in gray, and
  • deadlines coming soon (are “hot”) are highlighted in red.

See the GitHub readme document for examples of these. The app is actually better than a static list of text in web or slideshow in that it will dynamically format the deadlines, as listed above and shown in the GitHub readme.

The deadlines are stored, for each course, in a separate JSON file in the Documents/CourseDeadlines directory on the user’s Mac:

func store() throws {
	let storagePath = URL.documentsDirectory.appending(component: Deadlines.appDocumentDirectory, directoryHint: .isDirectory)
	deadlines.sort()
	let fileManager = FileManager.default
	let encoder = JSONEncoder()
	encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
	let data = try encoder.encode(self)
	let filePath = storagePath.appending(path: name + ".json").path(percentEncoded: false)
	print("Storing file \(filePath)")
	if !fileManager.createFile(atPath: filePath, contents: data) {
		throw DeadlineErrors.fileSaveError
	}
}

The design decision here was to make it easy to share the course deadlines — just send the JSON file to anyone with this app. Or another app anyone perhaps chooses to implement that is able to read the JSON file.

With the app, user can add end edit new courses, add and edit deadlines, and also delete deadlines and courses. Although the app was originally developed for a teacher (me), it could obviously be used also by students to manage the deadlines for the courses she takes.

I did present the deadlines with one course, weekly, using this app last Fall in the live lectures. I also sent a weekly newsletter in the course Moodle space and included a screenshot of the current deadline situation in the message.

I didn’t gather any data on the impact of the app. Did students actually miss the deadlines like in any other course? Cannot say. My main motivation was to make sure no one misses the deadlines because of not being aware of them. Hopefully the awareness of deadlines was improved, and there wasn’t (too much) annoyance in being repeatedly reminded of them, time and time again 😂

There are some Apple/Mac specific things in the JSON, an example of a course with four deadlines:

{
  "deadlines" : [
    {
      "becomesHotDaysBefore" : 7,
      "date" : 764027972.579756,
      "goal" : "Form pairs for course work",
      "isDealBreaker" : false,
      "symbol" : "person.2",
      "uuid" : "179C5A8D-2E3B-4000-A3F8-F821E98CDCAC"
    },
    {
      "becomesHotDaysBefore" : 7,
      "date" : 765464432.579756,
      "goal" : "GUI design finished",
      "isDealBreaker" : true,
      "symbol" : "document.on.clipboard",
      "uuid" : "E3A7AF6F-9322-4D6A-994C-346D5F1B6F43"
    },
    {
      "becomesHotDaysBefore" : 7,
      "date" : 767883632.579756,
      "goal" : "Learning tasks done",
      "isDealBreaker" : false,
      "symbol" : "checkmark.seal.fill",
      "uuid" : "DB7A7330-1CBD-4785-8FDD-AB5B5F35A596"
    },
    {
      "becomesHotDaysBefore" : 7,
      "date" : 768488432.579756,
      "goal" : "Implementation done",
      "isDealBreaker" : true,
      "symbol" : "hammer",
      "uuid" : "1AD3DBD3-4121-40EA-9C4D-368A10765999"
    }
  ],
  "name" : "Ohjelmointi 4",
  "startDate" : 763401692.579756,
  "uuid" : "33DCF108-1FAD-4DE7-9C6E-A9DC02073043"
}

The deadline date is not milliseconds from 1970 as integer (perhaps more common), but as double. The symbol for the deadline is a name for Apple SF Symbol. If implementing an app for other platforms to show the same JSON data, the developer needs to figure out a way to show something as the symbol (or ignore it). Maybe a custom drawn image or something.

I list some future improvement ideas in the GitHub readme, but don’t actually know if I am going to implement those. The app already fulfills the intended purpose, so anything else would be just something fun to implement, if I have the time or motivation to do them. One thing not listed in the readme is to provide the app also for iPhone/iPad, but since the original idea as a teacher tool used in lectures, did this only as a Mac app.

If you have any use for an app like that, or just want to use it for learning Swift/SwiftUI, the app is open sourced so just take it and use it.

Managing app settings with Java Properties

Many (most?) apps need some kind of settings the user can modify, that influence on the behavior of the app across app launches. In this post, I’ll show how to implement this with java.util.Properties class in a Java / Swing game.

As an example, I will use the Snakeses game from the previous post. In this game, user can change the difficulty, game mode and light/dark mode of the UI:

The selections will be saved to and read from a file named snakeses.ini:

#Snakeses Settings
#Tue May 06 19:28:32 EEST 2025
difficulty=hard
mode=dark
playername=Antti
style=classic

The settings file contains key-value pairs (e.g. on line difficulty=hard the difficulty is a key, and hard is the value). Managing the settings is then just handling these key-value pairs, providing a user a way to change these in a controlled manner, and then handling the saving and restoring the values with the settings file. The file can also contain commented lines (beginning with #) that are just for humans to read, and are not actual settings.

As you can see, also the latest player’s name, who entered the Hall of Fame, is also stored in the settings. Assuming the game is mostly played by the same player, she does not need to write her name again and again, but can just accept the notification as you can see in the previous post video.

The settings are managed in a single class, unsurprisingly named Settings:

public class Settings {
    public Settings() {
        difficulty = "easy";
        style = "modern";
        mode = "dark";
        playerName = "Anonymous";
    }

    public String difficulty;
    public String style;
    public String mode;
    public String playerName;

    public boolean isDirty = false;
    private static final String SETTINGS_FILE_NAME = "snakeses.ini";

As you can see, the constructor gives the default values to the various settings. You can also see that I’ve taken the easy road of letting the members be public instead of the default and recommended private. Lazy me, but I am myself making sure that as the only programmer in this project I will treat these public members responsibly and not mess the values with anything that is invalid. In a larger project I would let them be private and make sure that setter methods would check that the parameters to change the setting values would always be valid.

How to read the settings from the snakeses.ini file, is shown here:

public void read() throws IOException {
    File configFile = new File(SETTINGS_FILE_NAME);
    Properties config = new Properties();
    try (FileInputStream istream = new FileInputStream(configFile)) {
        config.load(istream);
        if (config.containsKey("difficulty")) {
            difficulty = config.getProperty("difficulty");
        } else {
            difficulty = "easy";
        }
        if (config.containsKey("style")) {
            style = config.getProperty("style");
        } else {
            style = "modern";
        }
        if (config.containsKey("mode")) {
            mode = config.getProperty("mode");
        } else {
            mode = "dark";
        }
        if (config.containsKey("playername")) {
            playerName = config.getProperty("playername");
        }
    } catch (FileNotFoundException e) {
        difficulty = "easy";
        style = "modern";
        mode = "dark";
        playerName = "Anonymous";
        save();
    } finally {
        isDirty = false;
    }
}

Opening and reading the settings file is handled using java.io.File, java.io.FileInputStream and java.util.Properties classes. The Properties object will then contain the settings read from the ini file, after calling config.load(istream).

After that, it is simple to read the setting values from the properties object by calling config.getProperty, giving the name of the property. Just to be sure, the code checks if the properties contain that key (using config.containsKey, and if it does not, a default value is used. This is necessary, since the file could be changed outside of the app, by the user, for example, so as a programmer you cannot trust that the file (after saving it) will surely contain those key-value pairs.

Also, when the app launches for the first time, the settings file is not there. It could be delivered with the app with default values, but again, nothing stops the user accidentally or deliberately deleting that file. So the code must prepare for the situation that the settings file does not exist — that’s why the catch (FileNotFoundException e). If the file is not there, again, resort to the default setting values. And then save the settings immediately, calling save(). That makes sure that at least now we have the settings file there.

The member variable isDirty is also set to false. isDirty is convenient to have. When the user opens up the settings panel and closes it, there is no sense in saving the settings if they were not changed. As you can see below, the isDirty is set to true if the settings change, and only then the settings are actually saved. No need to do any disk access if there is no need for it.

Do note that the app in this demo does not check if the settings are actually different from the original when user leaves the settings panel. If there are lots of settings and changing them leads to lots of code to be executed, you might want to keep the old settings somewhere, let the user to manipulate the settings, and then when the user is done, actually compare the old setting values to new ones. For those values that are actually different, then handle the changes to those settings only.

How about saving the settings:

public void save() throws FileNotFoundException, IOException {
    File configFile = new File(SETTINGS_FILE_NAME);
    Properties config = new Properties();
    try(FileOutputStream ostream = new FileOutputStream(configFile)) {
        config.setProperty("difficulty", difficulty);
        config.setProperty("style", style);
        config.setProperty("mode", mode);
        config.setProperty("playername", playerName);
        config.store(ostream, "Snakeses Settings");
    } finally {
        isDirty = false;
    }
}

Saving settings to a file is handled using java.io.File, java.io.FileOutputStream and java.util.Properties classes. In this case we just set the various properties of the Properties object, using config.setProperty, giving the key and value pairs to the configuration, and finally call config.store. As you can see, the string “Snakeses Settings” and the date of the update is saved as comments to the snakeses.ini file.

Also, the isDirty is set to false at this time; settings are now saved and not changed.

How the Settings panel then uses the settings when user is shown the current settings? As an example, let’s see how the game difficulty radio buttons are created and how the radio button corresponding to the current setting is selected, in SettingsPanel constructor, other details omitted:

public class SettingsPanel extends JPanel implements ActionListener {

    private Settings settings;

    public SettingsPanel(Settings settings) {
        this.settings = settings;

        ButtonGroup levelGroup = new ButtonGroup();
        JRadioButton easy = new JRadioButton("Easy");
        easy.setActionCommand("easy");
        easy.addActionListener(this);
        JRadioButton hard = new JRadioButton("Hard");
        hard.setActionCommand("hard");
        hard.addActionListener(this);
        levelGroup.add(easy);
        levelGroup.add(hard);
        if (settings.difficulty.equals("easy")) {
            easy.setSelected(true);
        } else {
            hard.setSelected(true);
        }

The “Easy” and “Hard” radio buttons are created to a ButtonGroup. That takes care of managing the principle of the related radiobuttos that only one of them can be selected at one time. The last if-else block shows how the current value of the Settings is used to select one or the other from these game difficulty radio buttons.

Did you know that the name “radio button” for this control comes from the actual radios to listen to radio stations? Radios used to have several buttons for selecting preselected radio station frequencies to listen to. Obviously, you can only listen to one station at a time. So pressing down one station button would then deselect (pop up) the previously selected station button.

That’s why these buttons are called “radio buttons”. Radio buttons in GUI frameworks are round to distinguish them from check boxes, which are rectangular and function differently (many of them can be selected simultaneously).

A vintage radio device with white radio buttons and two round controls to change the volume and control the frequency.
Row of seven white rectangular radio buttons in a vintage radio divide. Image from https://www.collectorsweekly.com/stories/273171-1950s-grundig-fleetwood-tube-radio-mode

Obviously, the controls to use in managing the settings depends on the setting and the possible values of that setting. For example, if we would have a setting with multiple values, like e.g. 42 distinct values, using radio buttons would not be a good choice. Then one could use a dropdown / combobox list instead, populating the available options to the list and selecting the one found in the settings.

What happens then when user changes the setting? In the code above, you can see that the SettingsPanel is set as the action listener to the radiobuttons, e.g. in easy.addActionListener(this). Also, each radio button were given a name for the command they represent, e.g. like this: easy.setActionCommand("easy").

The panel implements the ActionListener interface, and therefore must have the method actionPerformed overridden. Again, let’s look at the relevant parts of this method:

public void actionPerformed(ActionEvent e) {
    final String action = e.getActionCommand();
    if (action.equals("easy") || action.equals("hard")) {
        if (!settings.difficulty.equals(action)) {
            settings.difficulty = action;
            settings.isDirty = true;
        }
    } else if (action.equals("classic") || action.equals("modern")) {
// and the same for the other radio buttons...

Here, we check which was the action command related to the action event. So if the command was “easy” or “hard”, we check if the related setting value is different, we then change it and also set the isDirty of the setting to true indicating a need to actually save the settings to the snakeses.ini file as seen above.

So far we have seen how the Settings class manages the settings file, and how the SettingsPanel displays the current settings, and then changes the settings based on user actions.

How the app then actually initializes the settings and saves them if they have been changed? This is done, in this app, in the SnakesesApp class that contains the main method of the app.

When the app is launched, the settings object is the first one to be created:

    public static void main( String[] args )
    {
        javax.swing.SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                new SnakesesApp().run();
            }
        });        
    }
    private static SnakesesGame game;
    private static Settings settings;
    private static HallOfFame hallOfFame;

// Later...

private void run() {
    try {
        settings = new Settings();
        settings.read();
        hallOfFame = new HallOfFame();
        hallOfFame.read();
        game = new SnakesesGame(GameViewConstants.GAME_WIDTH, GameViewConstants.GAME_HEIGHT, settings);
        // GUI
        JFrame mainFrame = new JFrame("Snakeses");
// And the rest of the GUI is then created...

Things to note here: The app object contains the relevant application objects as member variables (game, settings, hall of fame). Also the GUI objects (frame, panels) are member variables, but left out from code snippet above since they are not relevant in this post.

As you can see, the settings are read from the settings file as the very first step of the application launch. Therefore, they are read from the file and available to the rest of the application immediately.

How about saving the settings? This could have been done in the SettingsPanel, but in this implementation it is done also in the SnakesesApp. The reason is, that changing some of the settings needs to be reflected in other components of the app. The method SnakesesApp.switchTo is called by the SettingsPanel when the close button is pressed. The app then switches back to the game view.

But before that, the app first checks if the settings were changed (isDirty is true), and saves the settings. App also instructs the hall of fame object to switch the hall of fame visible to the user, to the corresponding game difficulty level hall of fame data. This is because the game always shows the hall of fame for the current difficulty level only. This is shown in the code snippet below:

public static void switchTo(final View view) {
    CardLayout cardLayout = (CardLayout) (mainView.getLayout());
    if (settings.isDirty) {
        try {
            settings.save();                
            hallOfFame.setCurrentLevel(
                HallOfFame.Level.fromString(settings.difficulty)
            );
// ...

Other game objects and panels read the settings continuously as they do their things, so they need not to be updated the same way as the hall of fame needs to be. For example, the game panel that draws the snake and the food, uses the light/dark mode setting in painting the graphics each time the game screen is drawn:

public void paintComponent(Graphics g) {
	super.paintComponent(g);
	if (settings.mode.equals("dark") && getBackground().equals(Color.WHITE)) {
		setBackground(Color.BLACK);
	} else if (settings.mode.equals("light") && getBackground().equals(Color.BLACK)) {
		setBackground(Color.WHITE);
	}
// ....

Summarizing, we have now seen:

  • how the game settings and the settings file can be managed using the Java Property class,
  • how game settings panel can display and enable changing the settings,
  • how the app initializes the settings at lauch and how it saves the settings if they are changed in the settings panel,
  • how settings are used in the app when drawing the game screen, and
  • how changed settings can be propagated, if necessary, to other game elements like the hall of fame.

Snakeses demo

I implemented the classic Snake game (a.k.a “matopeli” in Finnish) using Java and Swing in retro style:

Why Java and Swing?

Originally, the idea was to implement something to demonstrate to students in our GUI design & implementation course. Namely, how to have one frame (window; JFrame) and be able to easily switch between different content views (JPanels) within that window, by using Swing CardLayout. And since Java and Swing are the default tools to use in this course (they can pick almost anything else they like, though), that’s why.

The game settings panel, hall of fame panel and the actual game view panel are all included in the same frame using the CardLayout. The demo also shows how to use keyboard shortcuts and buttons to switch between the three cards.

As usual for me, things got out of hand and I ended up implementing the whole game with all the features (I came up with). Unnecessary for the original purpose of the demo, but more fun than other more boring responsibilities, like various macrodata refinement types of tasks.

Since this app is “too complete” and could be used to copy-paste solutions in the course, the plan is not to publish this in source code. But parts of this can be included in (future) tutorials on how to:

  • use the CardLayout,
  • play the game using keyboard,
  • control the game using a Timer and TimerTask,
  • draw graphics on the screen,
  • store and restore settings using Properties, and
  • store and restore the hall of fame stats so that users cannot (at least easily) cheat by editing the hall of fame file manually.

The Hall of Fame file is scrambled using Base64 encoding and the Cesar cipher, so not actually rocket science. The scrambled Hall of Fame contents you can see in the video looks something like this, when scrambled:

OknoKOie4VrfFNtlmHHQ}gHQ7oHQ~YHR7InG4YnoKOie4VrfFNtlm...

Not something you can edit just by looking at it.

Anyone could try out implement the same scrambling algorithms (since I have now revealed all the important secrets about it!) and attempt to “decrypt” the file and then manually put themselves at the top, then “encrypt” it back into the file. They can then be proud no one ever reaches the same level of gameplay again… Damn those clever cheaters! Luckily one can always delete the file and start from scratch.

Thanks for reading and have a nice rest of your day!

Long break, status update & demo

So, another long break in blogging. This, of course, is due to having too much work and spending the remaining energy somewhere else (like running).

So I thought I’d write a couple of posts on what I have been doing in between the previous post in January:

  • Evaluating the Data Structures and Algorithms course programming assignments. This has been faster this year, since my automated tool that both unit tests all the submissions and now also checks the source code that it doesn’t use things students are not supposed to use, and does use things they are supposed to use. Later I’ll make a post about the latest and future improvements to this tool. I still use too much time on this, due to lacking teaching resources, so the only ways to improve the situation are to add more automation and forget about giving detailed feedback to students.
  • Launching and supervising six BSc student projects. This started in January and will continue to end of April or May, depending on individual project schedules. Groups of 4-6 students show the skills and knowledge they (hopefully) acquired during their BSc studies and design and/or implement a software for an external (or “external”) customer. Sometimes they continue development of already existing software. I have weekly status meetings with them and guide them through the project, and participate in official steering group meetings.
  • Preparing and conducting our MSc program applicant interviews. This took most of the February, almost full day.
  • In March, the Programming 4 course started. The course focuses on GUI design, usability and user experience. I am assisting my colleague in this course. Students design and implement a GUI app, often the first time in their lives. Up to this point, they have usually implemented only small command line apps with relatively small number of functions or classes. This week is the last week of the course.
  • Supervising some BSc and MSc thesis students and
  • Preparing courses for teaching in Fall.

So, lots of work and thus no time for blogging, considering my priorities.

Anyways, I thought that in addition to listing the jobs above, I could also show some demonstrations I prepared for the Programming 4 course. The one described below in this post is a very simple and small demonstration showing how to use a Publish/Subscribe pattern to update GUI views when a data object (a model) in software changes state.

The old school way to do this is the Observer design pattern, which has it’s pros and cons — easy to use but has its negative consequences too. Classes related to that in Java have been deprecated though still usable.

Instead of Observer, Java recommends using other techniques, like the Publish/Subscribe pattern, using Java Flow Publisher and Subscriber interfaces and classes.

So, I implemented a simple demo app in Java and Swing, WallClockApp (GitHub link to source). The app uses the Publish/Subsctibe pattern and looks like this:

You can add several timezones using the plus button, and see the time in these different timezones. The red circle and cross button in each timezone can be used to remove a timezone. Obviously, the times advance with each clock, as seconds tick forward.

The app follows the also common Model-View-Controller architecture in GUI apps. In the app the Model (class ClockTickSource) is an object that provides the time signal, once a second. The View objects (class WallClock) are the different time zone views (three visible in the screenshot above). The app object (class WallClockApp) acts as the Controller, creating the model, and adding clock Views for user selected timezones. The Views basically just show the time in their corresponding time zones.

Where does the Publish/Subscribe then fits into this? Well, the Model needs a way to tell the views when a second has passed. So it has to know about the views that need the time tick signal. When the Model’s timer ticks, once a second, it then needs to notify the views so that they can update the time visible in their timezones.

This has been implemented here using the Java Publish/Subscribe pattern. The model is the publisher, that publishes the current time in one second intervals. The current time is published as the Unix time, milliseconds since Jan 1st, 1970, in UTC time.

Views, the Subscribers, subscribe to the Publisher as they are created:

public WallClock(final ClockTickSource clock, final TimeZone timeZone) {
   super();
   setLayout( new BorderLayout());
   this.clock = clock;
   this.timeZone = timeZone;
   this.clock.subscribe(this);

On the last line above, you can see the View object subscribing to the clock ticks, calling the clock.subscribe. The Publisher (clock, the Model) then will notify the Subscriber about successful subscription and the View can then request for the clock ticks (Long values; the milliseconds from Jan 1st, 1970 UTC):

public void onSubscribe(final Subscription subscription) {
   this.subscription = subscription;
   subscription.request(Long.MAX_VALUE);
}

The Publisher then (code below) starts the clock ticking using a Timer, but only once, for the first subscriber (since only one timer is needed to run the show; see the start() method on details on how to launch the timer):

public void subscribe(final Subscriber<Long> subscriber) {
  if (publisher == null) {
    publisher = new SubmissionPublisher<Long>(ForkJoinPool.commonPool(), 10);
  }
  publisher.subscribe(subscriber);
  if (publisher.getNumberOfSubscribers() == 1) {
    start();
  }
}

A concrete Java class SubmissionPublisher does the actual work here in the Publisher side.

Then the subscribers (views) handle the published data (Unix timestamps) as they get the notification from the Publisher when onNext is called by the Java Flow framework. The views will then convert the UTC time in milliseconds in the parameter item to their respective timezones and show that to the user:

public void onNext(final Long item) {
   final long currentTime = item;
   Date time = new Date(currentTime);
   ZonedDateTime zonedTime = time.toInstant().atZone(timeZone.toZoneId());		
   timeLabel.setText(DateTimeFormatter.ofPattern("HH:mm:ss", Locale.getDefault()).format(zonedTime));
}

When the view closes (from the red circle/cross button), it needs to unsubscribe from the publisher, using the subscription.cancel():

public void close() {
   subscription.cancel();
   Container parent = getParent();
   parent.remove(this);
   parent.revalidate();
   parent.repaint();
}

You can get the rest of the details from the linked GitHub repository. This was a very small demo, just to show how the pub-sub works with the Java interfaces and classes. If you are interested in the details, head to the GitHub demo, clone it and check it out in action!

In the following posts I’ll show some other demonstrations and also write about the updates to the automatic DSA course testing/checking tool. Have a nice day!

Slowly out of Corona virus

I got the corona virus again, the second time for me. Previous infection was in August 2022, and then it was really bad. I had a terrible headache and very, very sore throat for two weeks. Couldn’t event swallow saliva without much pain, even though I ate the max amount of over-the-counter pain killers.

This time was not as bad but still quite bad. Partly the same symptoms but now it was more like a really bad flu with almost as bad headache and sore throat as last time. It’s almost three weeks since I got the first symptoms and it is still not over.

Anyways, I did have the energy to solve Advent of Code days 10 and 11 during this time, when I wasn’t yet feeling too bad. Just didn’t have the energy to write anything about the solutions here. Also managed to solve part 1 of day 12. Since then, nothing.

I still have cough, sneezing every now and then, a feeling of having a flu and am also very tired. The whole Xmas break, as well as new year, and all the days in-between and after was spent just being sick. Didn’t see any family or the kids and their families during the break, to avoid infecting them.

We had great plans to rest and recuperate after the busy work period from September to December; just rest mentally, go skiing and running, eat good food, go sauna, meet the family, etc. So that we’d be well rested when teaching begins next week.

Instead, we are just tired, consumed by this fucking virus. Hopefully no long covid comes out of this, that’d be too much.

Let’s see if I bother to write about the solved AoC puzzles and/or continue to solve the remaining puzzles later on. Because it seems that I have more student projects to supervise than last year, with larger groups. That seems to be a trend nowadays, each year more students, less resources (teachers and hours) to do the work, actually teaching students stuff. This is so bullshit but hey, what can you do.

The student projects start next week, when I am supposed to contact them all and kick them off. Also, next week I need to start grading the Data structures and algorithms course projects, as well as prepare the final exam for that course. That’s already a lot of work to do, in this shape I currently am. Going to take it easy…

Pausing

OK so there’s coronavirus in the house, and I am also getting ill. Hope that it is not as bad as the first time and there’s no long term consequences. Last time it was two weeks of headache from hell and very sore throat. I could barely swallow.

Continuing the advent of code later.

Advent of Code quick update

Solved Day 9 part 1 yesterday, but since then I’ve been caught with much work and haven’t had the opportunity to continue in spare/free time. So, I haven’t had the time nor energy to finish part 2.

This is the final teaching week in two programming courses, when students are getting quite active in making sure their work is either ready and acceptable, or they are too late in the schedule and are desperately asking for help to be able to proceed to meet the deadlines. It’s been quite busy, in Zoom breakout rooms, email and our internal support Q&A system…

So, this is probably going to be the AoC week when I’m going to fall behind. I’ll post updates when something gets done, but otherwise it’s radio silence until there is time for this “project”.

Advent of Code Day 8 – Resonant Collinearity

This was surprisingly easy! The part 1 of today’s task was correct on the first run, which is the first time this has happened to me! Usually I make stupid mistakes, not reading the puzzle carefully enough, and rushing to coding with too little planning ahead.

The puzzle part 1 was to find out positions on a map to place antinodes to prevent antennas to emit a signal that makes people 0.1% more likely to buy Easter Bunny brand Imitation Mediocre Chocolate as a Christmas gift.

Calculate the impact of the signal. How many unique locations within the bounds of the map contain an antinode?

A map looks like this:

............
........0...
.....0......
.......0....
....0.......
......A.....
............
............
........A...
.........A..
............
............

Where 0 and A are antennas that are sending on a same frequency, A antennas on their own and 0 antennas theirs, respectively. To prevent their signals, the antinodes ( #)must be placed in line of the antennas, after the first and last pair of antennas, like this:

......#....#
...#....0...
....#0....#.
..#....0....
....0....#..
.#....A.....
...#........
#......#....
........A...
.........A..
..........#.
..........#.

To solve the problem, I implented a Dictionary with key-value -pairs (the [Character: [Point]] below), where the key is an antenna with a specific frequency and value is an array of (x, y) positions on the map where that kind of antennas are placed.

var antinodes: Set<Point> = []
let matrix: Matrix<Character> = parse(data: data)
let antennaMap: [Character: [Point]] = parseAntennaMap(matrix)

After parsing the antennas and their positions into a Matrix, and extracting the antennas and their positions into the dictionary, I could print out the dictionary keys (antennas) with the values (a list of positions for the antennas with the same frequency ):

Antenna A
5:6
8:8
9:9
Antenna 0
1:8
2:5
3:7
4:4

So I could verify that everything is in order for the next step of the puzzle.

I already had a Point structure, having the x and y coordinates, as well as operations to calculate the difference of two points in the coordinate (the distance between them) as well as add together two coordinate points to extrapolate the line between two antennas to position the antinodes inline with the antennas.

So what to do with these? First I needed to handle all the antennas with the same frequency in pairs, to extrapolate the line between them to both directions to add the antinodes. The permutations(ofCount:) algorithm did that. For each pair of antennas, I determined which comes first (point is upper on the map compared to the other) and vice versa.

After that I counted the difference between the first and last point, because the antinode must be place an equal distance away from the antennas. Then I added a point above the first point (first - difference) and below the last point (last + difference).

Then I just added these two antinodes into a Set to get a number of antinodes placed on the map:

for (_, points) in antennaMap {
  let permutations = points.permutations(ofCount: 2)
  for permutation in permutations {
     let first =
     permutation.first! < permutation.last! ? permutation.first! : permutation.last!
    let last = first == permutation.first! ? permutation.last! : permutation.first!
    let difference = last - first
    antinodes.insert(first - difference)
    antinodes.insert(last + difference)
  }
}
antinodes = antinodes.filter({ matrix.contains(y: $0.y, x: $0.x) })
return antinodes.count

Finally I made sure no antinodes were placed outside the map coordinates (using filter to include only those points in the area of the matrix coordinates) and finally returning the count of antinodes placed (the puzzle answer).

Part 2 was just an addition to part 1. Instead of placing one antinode before and after the line of two antennas, place them so that the antinodes are repeated with equal spacing along the line formed by a pair of antennas, until to the edge of the map.

What I first missed in the puzzle instructions was that the positions of the antennas also count in the final answer, not just the antinodes. I did get that the unique positions only are counted (that is why the Set<Point> is used for antinode positions). As I realized this, I just formed a union of antinode and antenna positions, and count of those was the correct answer!

In total, this didn’t take long to solve, thanks to the existing data structures and algorithms, both from Swift, Swift libraries (Swift Algorithms) and my own utilities from previous years. Performance is also OK:

$ swift run -c release AdventOfCode 8 --benchmark
Building for production...
Build of product 'AdventOfCode' complete! (0.28s)
Executing Advent of Code challenge 8...
Part 1: 259
Part 2: 927
Part 1 took 0.000727958 seconds, part 2 took 0.000798333 seconds.

Advent of Code Day 7 – Bridge Repair

The goal for the day was to find out if summing or multiplication would produce the result of an equation, evaluating from left to right (not as usual in maths, evaluating multiplication first). For example:

190: 10 19

Here the result is 190, so it is obvious that multiplication is needed, 10 x 19 is 190. Things get a bit more complicated when there are more numbers:

3267: 81 40 27

Here it is possible to solve the correct result by 81 + 40 * 27 and 81 * 40 + 27 since they both equal 3267. Basically this is a problem where you want to try out different combinations, also called as permutations, of how to use summing and multiplication to produce the correct end result. The input contains some formulas that do not produce the correct result with any permutation, so those are ignored when producing the end sum of the game.

One way to solve this is using recursion – trying different combinations based on the previous evaluation step result with the current number and looking if, after evaluating all the numbers, this produces the correct result.

Simplified, the recursive part looks like the code below. Parameters are:

  • result: The expected end result of the evaluation.
  • intermediate: The intermediate result of the previous + and * operations on the previous numbers, and
  • numbers: the numbers not yet evaluated.

The return value is the result of the evaluation, either the correct result or zero if evaluation didn’t result in the correct result.

func evaluate(_ result: Int, _ intermediate: Int, _ numbers: [Int]) -> Int {
  if numbers.count == 1 {
   if intermediate + numbers.first! == result {
    return result
   } else if intermediate * numbers.first! == result {
    return result
   }
   return 0
  }
  
  let nextNumber = numbers.first!
  
  let summed = intermediate + nextNumber
  let multiplied = intermediate * nextNumber
  
  let evaluateSummed = evaluate(result, summed, Array(numbers.suffix(numbers.count - 1)), useConcatOperator: useConcatOperator)
  if evaluateSummed == result {
   return result
  }
  let evaluateMultiplied = evaluate(result, multiplied, Array(numbers.suffix(numbers.count - 1)), useConcatOperator: useConcatOperator)
  if evaluateMultiplied == result {
   return result
  }
  return 0
}

Recursion must always stop, otherwise it would continue forever – or actually, until stack memory available to the program runs out, producing a stack overflow.

Ending the recursion is handled first; if there is only one number left (the rightmost) we check if the intermediate result of the previous calculations together with the last number produce the expected result either by summing or multiplying. If not, return zero to indicate that this permutation of operations does not work correctly .

If there was more than one number left, both the sum and multiplication is calculated from the previous intermediate result, together with the current number. Then both intermediate values are used — recursively calling evaluate twice with sum and multiplied intermediate — with the rest of the numbers, checking if this produced the result we want. If not, code continues to the end of the function and returns zero — cannot do the thing with this permutation of operations.

I could have optimized the result, finishing the recursion earlier if the produced intermediary sum is greater than the end result. In that case, it is not useful to continue handing the numbers that are still left. Maybe I’ll add that later.

Second part of the puzzle added a third operator, concatenation. In addition to summing or multiplying, two numbers could be also concatenated, e.g. numbers 12 and 34 would result in number 1234.

This was easily added to the code above, just add another else if branch in the beginning and in the end part of the algorithm above, and execute those conditionally (if the function was called from part 2 solution).

Since the non-optimized version was already fast enough, haven’t done any optimization:

$ swift run -c release AdventOfCode 7 --benchmark
Building for production...
Build of product 'AdventOfCode' complete! (0.17s)
Executing Advent of Code challenge 7...
Part 1: 12940396350192
Part 2: 106016735664498
Part 1 took 0.023434208 seconds, part 2 took 0.590302166 seconds.

Now lunch, then some socializing, maybe continuing to Day 8 puzzles today or tomorrow, let’s see…