diff --git a/src/main/java/dotsandboxes/DotsAndBoxesUI.java b/src/main/java/dotsandboxes/DotsAndBoxesUI.java
new file mode 100644
index 0000000000000000000000000000000000000000..e646cc4ee68f674e0c2a6610ea5fd174927eaf41
--- /dev/null
+++ b/src/main/java/dotsandboxes/DotsAndBoxesUI.java
@@ -0,0 +1,139 @@
+package dotsandboxes;
+
+import javafx.scene.control.Label;
+import javafx.scene.layout.AnchorPane;
+import javafx.scene.paint.Color;
+import javafx.scene.shape.Circle;
+import javafx.scene.shape.Line;
+import javafx.scene.shape.Rectangle;
+
+public class DotsAndBoxesUI {
+
+    final int lineLength = 32;
+    final int margin = 10;
+    final int gap = 0;
+    final int dotDiameter = 6;
+
+    final DotsAndBoxesGrid grid;
+    final AnchorPane anchorPane;
+    final Label label;
+
+    /** Colours for the different players. Only goes up to 5. */
+    final Color[] playerColours = { Color.WHITE, Color.RED, Color.BLUE, Color.GREEN, Color.PURPLE, Color.ORANGE };
+
+    private void updateLabel() {
+        label.setTextFill(playerColours[grid.getPlayer()]);
+        label.setText(String.format("Player %d's turn", grid.getPlayer()));
+    }
+
+    public DotsAndBoxesUI(final DotsAndBoxesGrid grid) {
+        this.grid = grid;
+        anchorPane = new AnchorPane();
+
+        label = new Label("");
+        updateLabel();
+        grid.addConsumer((g) -> updateLabel());
+
+        // Size the anchorPane to just contain the elements
+        int width = margin + dotDiameter + gap + (grid.width - 1) * (gap + lineLength + gap + dotDiameter) + gap + margin;
+        int height = margin + dotDiameter + gap + (grid.height - 1) * (gap + lineLength + gap + dotDiameter) + gap + margin;
+        anchorPane.setPrefSize(width, height);
+
+
+        // Lay out the boxes
+        for (int row = 0; row < grid.height - 1; row++) {
+            for (int col = 0; col < grid.width - 1; col++) {
+                final int x = col;
+                final int y = row;
+                Rectangle box = new Rectangle(gap, gap, lineLength, lineLength);
+                box.setFill(Color.WHITE);
+
+                grid.addConsumer((g) -> {
+                    box.setFill(playerColours[g.getBoxOwner(x, y)]);
+                });
+
+                AnchorPane.setLeftAnchor(box, gap + dotDiameter + col * (gap + lineLength + gap + dotDiameter) + dotDiameter/2.0);
+                AnchorPane.setTopAnchor(box, gap + dotDiameter + row * (gap + lineLength + gap + dotDiameter) + dotDiameter/2.0);
+                anchorPane.getChildren().add(box);
+            }
+        }
+
+        // Lay out the horizontals
+        for (int row = 0; row < grid.height; row++) {
+            for (int col = 0; col < grid.width - 1; col++) {
+                Line line = new Line(0, dotDiameter/2.0, lineLength, dotDiameter/2.0);
+                line.setStrokeWidth(dotDiameter);
+                line.setStroke(Color.DARKGREY);
+
+                final int x = col;
+                final int y = row;
+                grid.addConsumer((g) -> {
+                    if (g.getHorizontal(x, y)) {
+                        line.setStroke(Color.BLACK);
+                    } else {
+                        line.setStroke(Color.LIGHTGRAY);
+                    }
+                });
+
+                line.setOnMouseClicked((evt) -> {try {
+                    grid.drawHorizontal(x, y, grid.getPlayer());
+                } catch (IllegalStateException ex) {
+                    // do nothing
+                    // This is a little artificial, as normally we'd implement this with a check that the line isn't
+                    // already "drawn" and then not calling the function. But for the exercise, we wanted students
+                    // to write a test that would ensure an exception is thrown, so we're relying on an exception
+                    // being thrown!
+                }});
+
+                AnchorPane.setLeftAnchor(line, 0.0 + gap + dotDiameter + col * (gap + lineLength + gap + dotDiameter));
+                AnchorPane.setTopAnchor(line, -dotDiameter/2.0 + gap + dotDiameter + row * (gap + lineLength + gap + dotDiameter));
+                anchorPane.getChildren().add(line);
+            }
+        }
+
+        // Lay out the verticals
+        for (int row = 0; row < grid.height - 1; row++) {
+            for (int col = 0; col < grid.width; col++) {
+                Line line = new Line(-dotDiameter/2.0, 0, -dotDiameter/2.0, lineLength);
+                line.setStrokeWidth(dotDiameter);
+                line.setStroke(Color.DARKGREY);
+
+                final int x = col;
+                final int y = row;
+                grid.addConsumer((g) -> {
+                    if (g.getVertical(x, y)) {
+                        line.setStroke(Color.BLACK);
+                    } else {
+                        line.setStroke(Color.LIGHTGRAY);
+                    }
+                });
+
+                line.setOnMouseClicked((evt) -> {try {
+                    grid.drawVertical(x, y, grid.getPlayer());
+                } catch (IllegalStateException ex) {
+                    // do nothing
+                }});
+
+                AnchorPane.setTopAnchor(line, 0.0 + gap + dotDiameter + row * (gap + lineLength + gap + dotDiameter));
+                AnchorPane.setLeftAnchor(line, -dotDiameter/2.0 + gap + dotDiameter + col * (gap + lineLength + gap + dotDiameter));
+                anchorPane.getChildren().add(line);
+            }
+        }
+
+        // Lay out the dots
+        for (int row = 0; row < grid.height; row++) {
+            for (int col = 0; col < grid.width; col++) {
+                Circle dot = new Circle(dotDiameter / 2.0);
+                dot.setFill(Color.YELLOW);
+
+                AnchorPane.setLeftAnchor(dot, gap + col * (gap + lineLength + gap + dotDiameter) + dotDiameter/2.0);
+                AnchorPane.setTopAnchor(dot, gap + row * (gap + lineLength + gap + dotDiameter) + dotDiameter/2.0);
+                anchorPane.getChildren().add(dot);
+            }
+        }
+
+    }
+
+
+
+}
diff --git a/src/main/java/dotsandboxes/Main.java b/src/main/java/dotsandboxes/Main.java
new file mode 100644
index 0000000000000000000000000000000000000000..cd2bce88d0715271aba46b8947f1d903ec301a1b
--- /dev/null
+++ b/src/main/java/dotsandboxes/Main.java
@@ -0,0 +1,40 @@
+package dotsandboxes;
+
+import javafx.application.Application;
+import javafx.scene.Scene;
+import javafx.scene.control.Label;
+import javafx.scene.layout.BorderPane;
+import javafx.stage.Stage;
+
+/** Our main class that launches the app. */
+public class Main extends Application {
+
+    DotsAndBoxesGrid grid = new DotsAndBoxesGrid(15, 8, 2);
+
+    @Override
+    public void start(Stage primaryStage) throws Exception {
+        primaryStage.setTitle("Dots and Boxes");
+
+        // FIXME: Update this label to show your name and student number
+        Label label = new Label("Name: (Your name and student number goes here)");
+
+        BorderPane borderPane = new BorderPane();
+        borderPane.setBottom(label);
+        Scene scene = new Scene(borderPane, 600, 400);
+
+        DotsAndBoxesUI dbUi = new DotsAndBoxesUI(grid);
+        borderPane.setCenter(dbUi.anchorPane);
+        borderPane.setTop(dbUi.label);
+
+        primaryStage.setScene(scene);
+
+        primaryStage.show();
+
+        // This sets what to do when we close the main window.
+        // Notice that we are using a "lambda function" (i.e., an anonymously defined function defined within the
+        // call to setOnCloseRequest). These are very useful in GUI code and we'll probably see a lot of them in the
+        // project.
+        primaryStage.setOnCloseRequest((evt) -> System.exit(0));
+    }
+
+}
diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java
new file mode 100644
index 0000000000000000000000000000000000000000..e51a2313046b8aa9453a946e97237e0e8f298fae
--- /dev/null
+++ b/src/main/java/module-info.java
@@ -0,0 +1,19 @@
+/**
+ * This is a module file.
+ *
+ * Modules were introduced in Java 9, allowing modules to express dependencies on other modules and to publish their
+ * public packages.
+ *
+ * Using modules makes things easier when working with JavaFX, because JavaFX is itself published as a module.
+ *
+ * Please note: "module" in Java is not quite the same as "dependency" in Gradle. They are similar concepts, but
+ * "modules" is part of the Java language (but doesn't express how to find them on the internet) whereas
+ * "dependencies" is part of the Gradle build system and expresses how to find them on the internet (but does not
+ * specify the module imports to the Java compiler). We need both.
+ */
+module dotsAndBoxes {
+    requires javafx.graphics;
+    requires javafx.controls;
+    requires org.apache.logging.log4j;
+    exports dotsandboxes;
+}
\ No newline at end of file
diff --git a/src/test/resources/log4j2.xml b/src/test/resources/log4j2.xml
new file mode 100644
index 0000000000000000000000000000000000000000..fc48a88519bd2c9dc4af3e86af8e2ce0c18b2486
--- /dev/null
+++ b/src/test/resources/log4j2.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Configuration status="WARN">
+    <Appenders>
+        <Console name="Console" target="SYSTEM_OUT">
+            <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
+        </Console>
+    </Appenders>
+    <Loggers>
+        <Root level="debug">
+            <AppenderRef ref="Console"/>
+        </Root>
+    </Loggers>
+</Configuration>
\ No newline at end of file