From b08b8a53d533eaa6104316de584e26f60ffae355 Mon Sep 17 00:00:00 2001
From: William Billingsley <wbillingsley@cantab.net>
Date: Sat, 8 Apr 2023 14:25:27 +1000
Subject: [PATCH] Updated to 2023 assignment task

---
 README.md                                     | 160 ++++--------------
 build.sbt                                     |   8 +-
 project/build.properties                      |   2 +-
 project/plugins.sbt                           |   1 -
 sbt-override.sh                               |   1 -
 src/main/scala/cosc250/boids/Boid.scala       | 137 ---------------
 src/main/scala/cosc250/boids/BoidsApp.scala   | 134 ---------------
 src/main/scala/cosc250/boids/BoidsPanel.scala |  67 --------
 .../scala/cosc250/boids/FrameMemory.scala     |  27 ---
 .../cosc250/boids/SimulationController.scala  |  80 ---------
 .../scala/cosc250/boids/SimulationFrame.scala |  57 -------
 src/main/scala/cosc250/boids/Vec2.scala       | 117 -------------
 src/main/scala/cosc250/reversi/App.scala      | 149 ++++++++++++++++
 src/main/scala/cosc250/reversi/Reversi.scala  |  63 +++++++
 src/test/scala/cosc250/boids/BoidSuite.scala  |  29 ----
 .../cosc250/boids/SimulationFrameSuite.scala  |  12 --
 src/test/scala/cosc250/boids/Vec2Suite.scala  |  37 ----
 .../scala/cosc250/reversi/ReversiSuite.scala  |  31 ++++
 18 files changed, 286 insertions(+), 826 deletions(-)
 delete mode 100644 project/plugins.sbt
 delete mode 100755 sbt-override.sh
 delete mode 100644 src/main/scala/cosc250/boids/Boid.scala
 delete mode 100644 src/main/scala/cosc250/boids/BoidsApp.scala
 delete mode 100644 src/main/scala/cosc250/boids/BoidsPanel.scala
 delete mode 100644 src/main/scala/cosc250/boids/FrameMemory.scala
 delete mode 100644 src/main/scala/cosc250/boids/SimulationController.scala
 delete mode 100644 src/main/scala/cosc250/boids/SimulationFrame.scala
 delete mode 100644 src/main/scala/cosc250/boids/Vec2.scala
 create mode 100644 src/main/scala/cosc250/reversi/App.scala
 create mode 100644 src/main/scala/cosc250/reversi/Reversi.scala
 delete mode 100644 src/test/scala/cosc250/boids/BoidSuite.scala
 delete mode 100644 src/test/scala/cosc250/boids/SimulationFrameSuite.scala
 delete mode 100644 src/test/scala/cosc250/boids/Vec2Suite.scala
 create mode 100644 src/test/scala/cosc250/reversi/ReversiSuite.scala

diff --git a/README.md b/README.md
index 117c4cd..dd86d98 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# Assignment 2: Boids
+# Assignment 2: Reversi
 
 In a pure functional language, such as Haskell, the code is a pure (no side-effects) description of a program which is
 then run in a runtime environment.
@@ -12,132 +12,52 @@ We're not going to go quite that far - this is only your second assignment in th
 to separate our functional code from our imperative code somewhat. And we're going to try to make the "important"
 code functional (and more testable).
 
-We're going to do a simulation where:
+In this case, we're going to use functional programming to write a little classical AI for the game of Reversi. 
+The AI will be able to look ahead a variable number of moves to pick the best move to take.
+The AI code (code in `Reversi.scala`) should be pure and functional.
 
-* The core logic of the simulation (the current frame, and the "frame memory" storing previously calculated frames) is
-  written functionally.
-* The UI and a SimulationController class are written imperatively.
+The UI has been written for you (in `App.scala`), and does include some mutation.
 
-The simulation is *boids*.
+## Reversi
 
-## Boids
+[Reversi](https://en.wikipedia.org/wiki/Reversi) is a traditional game played on an 8x8 board.
 
-Boids ("bird-oids") is a simulation of flocking introduced in a paper by
-Craig Reynolds in 1987. You can see an online version of how these boids
-behave on the Processing site, here:
+There are two players, black and white, who take turns placing disks on the board.
 
-https://processing.org/examples/flocking.html
+The first two moves for each player involve placing a disk in one of the four central squares of the board. For these two moves,
+no captures are made. (We're doing the original 1883 rules.) Programming hint: after these moves, there are four pieces on the board.
 
-Each boid has a position and a velocity. At each step, the boid will 
-move according to its velocity
+For all subsequent moves, a move is valid if it captures at least one piece. (See the examples on the wikipedia page, or try playing it online.)
+Note that captures can be horizontal, vertical, or diagonal, and a move might capture pieces on multiple axes.
 
-    newPosition = position + velocity
+If a player has no valid moves, their turn is skipped.
 
-And change velocity based on a calculated acceleration
+If neither player has a valid move, the game ends and the player with the most pieces on the board wins.
 
-    newVelocity = velocity + acceleration
-    
-Note that there's no measure of how long the timestep is. 
+Note: You are *not writing this for humans to play*.
 
-The acceleration is generally calculated as a mix of three forces for each
-boid:
+## Writing the simulation
 
-* Separation - if other boids are too close, it will try to move away from them
-* Alignment - it will try to match the velocity and direction of other boids within 50 pixels
-* Cohesion - it will try to steer towards the middle of its neighbours (where "neighbours" is all boids within 50 pixels)
+You are writing a model of the game in which the computer will play itself. A widget on the UI will let us alter the "lookahead" setting for the AI.
 
-Boids have a maximum velocity, and a maximum acceleration they can apply when steering
+* If Lookahead is 0, it will play a random valid move
+* If Lookahead is 1, it will play the move that leaves it with the most pieces after playing it.
+* If Lookahead is 2, it will play the move that leaves it with the most pieces after its opponent has played the best move with Lookahead of 1
+* If Lookahead is 3, it will play the move that leaves it with the most pieces after its opponent has played the best move with Lookahead of 2
+* etc.
 
-In our assignment 2, you're going to be implementing an augmented boids
-simulation using a mixture of functional and imperative programming.
+(Though you'll notice the mark scheme only goes so far as to try lookahead of 0, 1, and 2.)
 
-## Mixing functional and imperative programming
+The UI will ask your code to play a turn about once per second. 
 
-* Everything in `FrameMemory`, `SimulationFrame`, and `Boid` is pure and functional
-* `SimulationController` and the UI classes are imperative.
-
-There are also various properties that we're going to add to our simulation:
-
-* The wind
-* The ability to "startle" the boids by applying a random impulse to
-  each boid in the next frame
-* The ability to tell the simulation that at the next frame, it should
-  add a boid
-
-## What's provided
-
-I have provided most of the classes, but I've stripped out some of their
-implementation.
-
-Fully implemented, you have:
-
-* `Vec2` -- a class that can do vector addition and multiplication. So
-   that if you say `newPosition = position + velocity` it just works.
-  
-* `BoidsPanel` -- a panel that knows how to draw a `Seq[Boid]`
-
-  
-Partially implemented, you have: 
-   
-* `Boid` -- an immutable representation of a Boid. You will need to
-   work functionally to produce new sequences of `Boid`
-
-* extension methods for `Seq[Boid]` that I think will make your algorithms in `Boid` cleaner to write
-
-* `SimulationFrame`, which holds a `Seq[Boid]` but can also report various statistics on it
-
-* `FrameMemory`, which holds a memory of the last *n* frames of the simulation
-   
-* `SimulationController` -- a class the UI buttons and timer can call to control and run the simulation.
-
-* `BoidsApp` -- the runnable program, that creates a window, and adds the
-  `BoidsPanel` and some buttons. Note that the buttons' event handlers 
-  have not all yet been implemented.
-  
-## What the components need to do
-
-* The **Action Replay** button must take the simulation back to the oldest frame in  
-  the memory buffer (typically one second) and let the simulation continue 
-  again from there.  
-  Hint: think about which end you want to insert the frame.
-  
-* The **wind** buttons should set the wind strength and direction. As the
-  boids' velocity is normalised on the next tick, in practice this works as a
-  fairly effective flock steering mechanism. If you set the wind blowing right, 
-  you should see the flock sweep around to follow the wind.
-  
-* The **Startle boids** button should cause the simulation, at the next time
-  step, to perturb each boid by a velocity of `startleStrength` in a random 
-  direction (a different random direction for each boid)
-  
-* The **Regenesis** button should push a frame into the queue with `numBoids` boids heading from the
-  centre in random direction. Note this shoud *not* clear the queue -- hitting
-  **Action Replay** quickly after hitting **Regenesis** should jump back
-  into one of the old states. 
-
-* Clicking a point on the canvas should add a new boid into the simulation, 
-  heading in a random direction with a velocity of 1
-
-* There are also three labels at the foot of the simulation, which are intended to show some
-  data about the flock. (These are drawn from `Simulation.flockDir`, `Simulation.flockSpeed`, and `Simulation.flockSep`
-  which you'll need to implement.)
-
-  - Flock direction: The average direction of the flock. (Sum the velocity vectors and take its theta). You should be
-    able to see if this is working by seeing how it changes with the wind buttons
-
-  - Flock speed: The average speed of the flock. (Take the average of the magnitude of the velocity vectors). This should
-    stay more or less constant, because of how boids work
-
-  - Flock separation: The variance of the position of the flock. This one's a little trickier. Find the centroid of the flock
-    (average the position vectors). Then for each boid, calculate the square of its distance from the centroid. Return the 
-    mean of that. To test this, hit the "explosion of boids" button. It should drop to nearly zero and then grow.
+There is also a history of moves. Clicking in this history of moves will ask your simulation to go back in time as if that move had just been played.
+You will notice that the UI wants to keep an immutable `Seq[GameState]` rather than just a game state, for this purpose.
 
 ## Tests
 
-There are 3 marks for writing tests of functional components in your code (2 each for `Boid`, `Seq[Boid]`, and 
-`SimulationFrame` and/or `FrameMemory`). The point of this is not to ensure broad test coverage - this is not a 
-test-driven development unit - but to give you a little experience in how making code functional and composable makes it 
-amenable to unit testing.
+There are 2 marks for implementing unit tests for counting pieces of each player and detecting when the game is over.
+
+You will probably also find it helpful to write your own unit tests for other aspects as you develop your code.
 
 ## Marking
 
@@ -146,20 +66,14 @@ more formative and open-ended.
 
 Functionality: 
 
-* The boids simulation works: 6
-* Adding a boid by clicking the canvas works: 1
-* Wind works: 1
-* Startle works: 1
-* Regenesis works: 1 
-* Action replay works: 1
-* Mean direction, separation, and velocity works: 1 (together)
-
-Tests you've written:
-
-* At least 2 tests for Boid: 1
-* At least 2 tests for extension methods on Seq[Boid]: 1
-* At least 2 tests for SimulationFrame and/or FrameMemory: 1
+* The Reversi simulation works & plays valid moves: 6
+* Rewind (clicking in the move history list to rewind to that point in the game) works: 1
+* Play with lookahead 0 appears to work: 1
+* Play with lookahead 1 appears to work: 1
+* Play with lookahead 2 appears to work: 1
+* Test for counting pieces implemented and code works: 1
+* Test for gameOver implemented and code works: 1 
 
 Quality: 
 
-* Overall quality judgment (functional, readable, tidy, concise): 5
+* Overall quality judgment (functional, readable, tidy, concise): 3
diff --git a/build.sbt b/build.sbt
index 779880c..cf57f57 100644
--- a/build.sbt
+++ b/build.sbt
@@ -1,10 +1,12 @@
 lazy val root = (project in file(".")).
   settings(
-    name := "Boids",
-    version := "2022.0",
+    name := "Reversi",
+    version := "2023.0",
     scalaVersion := "3.1.0"
   )
 
-libraryDependencies += "org.scala-lang.modules" %% "scala-swing" % "3.0.0"
+run / fork := true
+
+libraryDependencies += "org.scalafx" %% "scalafx" % "20.0.0-R31"
 libraryDependencies += "org.scalameta" %% "munit" % "0.7.29" % Test
 testFrameworks += new TestFramework("munit.Framework")
diff --git a/project/build.properties b/project/build.properties
index c8fcab5..46e43a9 100644
--- a/project/build.properties
+++ b/project/build.properties
@@ -1 +1 @@
-sbt.version=1.6.2
+sbt.version=1.8.2
diff --git a/project/plugins.sbt b/project/plugins.sbt
deleted file mode 100644
index 0e65e30..0000000
--- a/project/plugins.sbt
+++ /dev/null
@@ -1 +0,0 @@
-// No plugin needed for Scala 3
\ No newline at end of file
diff --git a/sbt-override.sh b/sbt-override.sh
deleted file mode 100755
index 5aa0c0d..0000000
--- a/sbt-override.sh
+++ /dev/null
@@ -1 +0,0 @@
-sbt -Dsbt.override.build.repos=true
diff --git a/src/main/scala/cosc250/boids/Boid.scala b/src/main/scala/cosc250/boids/Boid.scala
deleted file mode 100644
index 8efc467..0000000
--- a/src/main/scala/cosc250/boids/Boid.scala
+++ /dev/null
@@ -1,137 +0,0 @@
-package cosc250.boids
-
-/**
-  * A boid (bird-oid). It has a position and a velocity.
-  *
-  *
-  * https://processing.org/examples/flocking.html
-  */
-case class Boid(
-  position:Vec2, velocity:Vec2
-) {
-
-  /**
-    * Calculates an acceleration vector that will cause it to maintain a minimum
-    * separation from its closest neighbours
-    * This steer is limited to maxForce
-    */
-  def separate(others:Seq[Boid]):Vec2 = {
-    ???
-  }
-
-  /**
-    * Calculates an acceleration vector that will cause it align its direction and
-    * velocity with other birds within Boid.neighbourDist
-    * This alignment force is limited to maxForce
-    */
-  def align(others:Seq[Boid]):Vec2 = {
-    ???
-  }
-
-  /**
-    * Calculates an acceleration that will steer this boid towards the target.
-    * The steer is limited to maxForce
-    */
-  def seek(targetPos:Vec2):Vec2 = {
-    ???
-  }
-
-
-  /**
-    * Calculates an acceleration that will keep it near its neighbours and maintain
-    * the flock cohesion
-    */
-  def cohesion(others:Seq[Boid]):Vec2 = {
-    ???
-  }
-
-
-  /**
-    * Calculates a flocking acceleration that is a composite of its separation,
-    * align, and cohesion acceleration vectors.
-    */
-  def flock(others:Seq[Boid]):Vec2 = {
-    ???
-  }
-
-  /**
-    * Produces a new Boid by adding the boid's velocity to its position, and adding
-    * the acceleration vector to the boid's velocity. Note that there is no division
-    * by timestep -- it's just p = p + v, and v = v + a
-    *
-    * Also note that we don't apply the limiting on maxForce in this function -- this is
-    * so that the startle effect can dramatically perturb the birds in a way they would
-    * not normally be perturbed in flight. Instead, limit maxForce in the flock function
-    * (or the functions it calls)
-    *
-    * We do, however, limit a boid's velocity to maxSpeed in this function. But we do it
-    * *before* we add the influence of the wind to the boid's velocity -- it's possible
-    * to fly faster downwind than upwind.
-    */
-  def update(acceleration:Vec2, wind:Vec2):Boid = {
-    ???
-  }
-
-  def wrapX(x:Double):Double = {
-    if (x > Boid.maxX) x - Boid.maxX else if (x < 0) x + Boid.maxX else x
-  }
-
-  def wrapY(y:Double):Double = {
-    if (y > Boid.maxY) y - Boid.maxY else if (y < 0) y + Boid.maxY else y
-  }
-}
-
-object Boid {
-  /** How far apart the boids want to be */
-  val desiredSeparation = 25
-
-  /** Maximum flying velocity of a boid */
-  val maxSpeed = 2
-
-  /** maximum accelaration of a boid */
-  val maxForce = 0.03
-
-  /** Other boids within this range are considered neighbours */
-  val neighBourDist = 50
-
-  /** Wrap width of the simulation. ie, for any Boid, 0 <= x < 640 */
-  def maxX:Int = SimulationController.width
-
-  /** Wrap height of the simulation. ie, for any Boid, 0 <= y < 480 */
-  def maxY:Int = SimulationController.height
-
-  /** When the boids are startled, the strength of the vector that is applied to each of them */
-  val startleStrength:Double = Boid.maxSpeed
-
-  /** A function that will "startle" a boid */
-  def startleFunction(b:Boid):Vec2 = ???
-
-}
-
-/*
- * Defining these extension methods might make your work in the Boids algorithms cleaner.
- */
-extension (boids:Seq[Boid]) {
-
-  /**
-    * Returns only those boids within d distance of position p
-    * align, separate, and cohesion all want to consider boids within a certain range.
-    */
-  def closeTo(p:Vec2, d:Double):Seq[Boid] =
-    ???
-
-  /**
-    * Calculates the centroid of a group of boids.
-    * Cohesion asks a boid to steer towards the centroid of the boids within a certain distance
-    */
-  def centroid:Vec2 =
-    ???
-
-  /**
-    * Calculates the average velocity vector (add them up and divide by the number in the group) of a group of boids
-    * Align asks a boid to steer so it will align more with its neighbours' average velocity vector
-    */
-  def averageVelocity:Vec2 =
-    ???
-
-}
diff --git a/src/main/scala/cosc250/boids/BoidsApp.scala b/src/main/scala/cosc250/boids/BoidsApp.scala
deleted file mode 100644
index ee2693a..0000000
--- a/src/main/scala/cosc250/boids/BoidsApp.scala
+++ /dev/null
@@ -1,134 +0,0 @@
-package cosc250.boids
-
-import java.awt.event.{MouseAdapter, MouseEvent, MouseListener}
-import java.awt.{BorderLayout, Dimension, FlowLayout, GridLayout}
-import javax.swing._
-
-import scala.collection.mutable
-import scala.util.Random
-
-/**
- * You shouldn't edit this file
- */
-object BoidsApp {
-
-  /** The main window */
-  val window = new JFrame()
-
-  /** A panel to render our boids */
-  val boidsPanel = new BoidsPanel
-
-  /** Action replay button */
-  val replay = new JButton("Action Replay")
-
-  val nwWind = new JButton("<html>&#x2198;</html>")
-  val nWind = new JButton("<html>&darr;</html>")
-  val neWind = new JButton("<html>&#x2199;</html>")
-  val wWind = new JButton("<html>&rarr;</html>")
-  val stopWind = new JButton("<html>&times;</html>")
-  val eWind = new JButton("<html>&larr;</html>")
-  val swWind = new JButton("<html>&#x2197;</html>")
-  val sWind = new JButton("<html>&uarr;</html>")
-  val seWind = new JButton("<html>&#x2196;</html>")
-
-  val startle = new JButton("Startle boids")
-
-  val regenesis = new JButton("Regenesis")
-
-  val directionLabel = new JLabel("Flock direction: xx radians")
-  val velocityLabel = new JLabel("Flock speed: xx")
-  val separationLabel = new JLabel("Flock separation: xx")
-
-  @main def run = {
-
-    val container = new JPanel()
-    container.setLayout(new BorderLayout())
-    container.add(boidsPanel, BorderLayout.CENTER)
-
-    val controlsContainer = Box.createVerticalBox()
-
-    val windPanel = new JPanel()
-    windPanel.setLayout(new GridLayout(3, 3))
-    windPanel.add(nwWind)
-    windPanel.add(nWind)
-    windPanel.add(neWind)
-    windPanel.add(wWind)
-    windPanel.add(stopWind)
-    windPanel.add(eWind)
-    windPanel.add(swWind)
-    windPanel.add(sWind)
-    windPanel.add(seWind)
-    windPanel.setAlignmentX(0)
-
-    val windStrength = SimulationController.windStrength
-
-    nwWind.addActionListener((_) => SimulationController.setWindDirection(Vec2.NW))
-    nWind.addActionListener((_) => SimulationController.setWindDirection(Vec2.N))
-    neWind.addActionListener((_) => SimulationController.setWindDirection(Vec2.NE))
-    wWind.addActionListener((_) => SimulationController.setWindDirection(Vec2.W))
-    stopWind.addActionListener((_) => SimulationController.wind=None)
-    eWind.addActionListener((_) => SimulationController.setWindDirection(Vec2.E))
-    swWind.addActionListener((_) => SimulationController.setWindDirection(Vec2.SW))
-    sWind.addActionListener((_) => SimulationController.setWindDirection(Vec2.S))
-    seWind.addActionListener((_) => SimulationController.setWindDirection(Vec2.SE))
-
-    val wc = new JLabel("Wind controls")
-    wc.setAlignmentX(0)
-    controlsContainer.add(wc)
-    controlsContainer.add(windPanel)
-
-
-    controlsContainer.add(new JLabel("Actions"))
-    controlsContainer.add(replay)
-    controlsContainer.add(startle)
-    controlsContainer.add(regenesis)
-    controlsContainer.add(new JPanel())
-
-    container.add(controlsContainer, BorderLayout.EAST)
-
-    val dataContainer = new JPanel()
-    dataContainer.setLayout(new GridLayout(1, 3))
-    dataContainer.add(directionLabel)
-    dataContainer.add(velocityLabel)
-    dataContainer.add(separationLabel)
-
-    container.add(dataContainer, BorderLayout.SOUTH)
-
-
-    window.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE)
-    window.add(container)
-    window.setSize(container.getPreferredSize)
-    window.setVisible(true)
-
-    replay.addActionListener({ (evt) =>
-      SimulationController.resetQueue()
-    })
-
-    startle.addActionListener({ (evt) =>
-      SimulationController.oneTimeFunction = Some(Boid.startleFunction)
-    })
-
-    regenesis.addActionListener({ (evt) =>
-      SimulationController.pushFrame(SimulationFrame.explosionOfBoids(SimulationController.numBoids))
-    })
-
-    boidsPanel.addMouseListener(new MouseAdapter {
-      override def mouseClicked(e: MouseEvent):Unit = {
-        SimulationController.pushBoid(Boid(Vec2(e.getX, e.getY), Vec2.randomDir(1)))
-      }
-    })
-
-    val timer = new Timer(16, (e) => {
-      SimulationController.update()
-      boidsPanel.setBoids(SimulationController.current.boids)
-      SwingUtilities.invokeLater(() =>
-        directionLabel.setText(s"Flock direction: ${SimulationController.current.flockDir} radians")
-        velocityLabel.setText(s"Flock speed: ${SimulationController.current.flockSpeed} ")
-        separationLabel.setText(s"Flock separation: ${SimulationController.current.flockSep}")
-      )
-    })
-    timer.start()
-
-  }
-
-}
diff --git a/src/main/scala/cosc250/boids/BoidsPanel.scala b/src/main/scala/cosc250/boids/BoidsPanel.scala
deleted file mode 100644
index aa29367..0000000
--- a/src/main/scala/cosc250/boids/BoidsPanel.scala
+++ /dev/null
@@ -1,67 +0,0 @@
-package cosc250.boids
-
-import java.awt._
-import javax.swing.JPanel
-
-/**
-  * A panel that knows how to paint a sequence of Boids
-  *
-  * Note, this is a JPanel and has methods such as addMouseListener
-  */
-class BoidsPanel extends JPanel {
-
-  val backgroundColour:Color = Color.DARK_GRAY
-
-  /**
-    * Updates the sequence of boids that will be painted, and queues a repaint to happen on the next render cycle
-    */
-  def setBoids(bs:Seq[Boid]) = {
-    boids = bs
-    this.repaint()
-  }
-
-  /** Contains the boids we will be rendering on the next render cycle */
-  private var boids:Seq[Boid] = Seq.empty
-
-  override def getMinimumSize = new Dimension(SimulationController.width, SimulationController.height)
-  override def getPreferredSize = new Dimension(SimulationController.width, SimulationController.height)
-  override def getMaximumSize = new Dimension(SimulationController.width, SimulationController.height)
-
-  /** Paints our boids */
-  override def paintComponent(gAwt:Graphics): Unit = {
-    val g = gAwt.asInstanceOf[Graphics2D]
-
-    /** A boid is a triangle */
-    def boidShape:Shape = {
-      new Polygon(Array(-4, 4, -4), Array(-3, 0, 3), 3)
-    }
-
-    /** Paints a single boid */
-    def paintBoid(b:Boid):Unit = {
-      // Remember the old graphics transform so we can reset it
-      val xform = g.getTransform
-
-      // Set the transform to the boid's position, pointed in the direction of the
-      // boid's velocity
-      val Vec2(bx, by) = b.position
-      g.translate(bx, by)
-      g.rotate(b.velocity.theta)
-
-      // draw a small triangle
-      g.setColor(Color.WHITE)
-      g.draw(boidShape)
-
-      // Reset the graphics transform
-      g.setTransform(xform)
-    }
-
-    // Clear the backgound
-    g.setColor(backgroundColour)
-    g.fillRect(0, 0, getWidth, getHeight)
-
-    // Paint every boid
-    boids.foreach(paintBoid)
-
-  }
-
-}
diff --git a/src/main/scala/cosc250/boids/FrameMemory.scala b/src/main/scala/cosc250/boids/FrameMemory.scala
deleted file mode 100644
index 602f034..0000000
--- a/src/main/scala/cosc250/boids/FrameMemory.scala
+++ /dev/null
@@ -1,27 +0,0 @@
-package cosc250.boids
-
-import scala.collection.immutable.Queue
-
-/**
-  * Holds the replay buffer for our simulation.
-  *
-  * @param queue - the queue of frames that is the memory
-  * @param max - the max number of frames to hold
-  */
-class FrameMemory(queue:Queue[SimulationFrame], max:Int) {
-
-  /** An alternative constructor, so we can say FrameMemory(startFrame, maxFrames) */
-  def this(startFrame:SimulationFrame, max:Int) = this(Queue(startFrame), max)
-
-  def currentFrame:SimulationFrame =
-    // Remember, items join queues at the back.
-    ???
-
-  def oldestFrame:SimulationFrame =
-    ???
-
-  def pushFrame(frame:SimulationFrame):FrameMemory =
-    // Don't forget to dequeue old frames if it's getting too long.
-    ???
-
-}
diff --git a/src/main/scala/cosc250/boids/SimulationController.scala b/src/main/scala/cosc250/boids/SimulationController.scala
deleted file mode 100644
index 7beaaeb..0000000
--- a/src/main/scala/cosc250/boids/SimulationController.scala
+++ /dev/null
@@ -1,80 +0,0 @@
-package cosc250.boids
-
-/**
-  * We've kept mutation in the simulation just in this top-level object.
-  */
-object SimulationController {
-
-  /** Wrap width of the simulation. ie, for any Boid, 0 <= x < 640 */
-  val width = 640
-
-  /** Wrap height of the simulation. ie, for any Boid, 0 <= y < 480 */
-  val height = 480
-
-  /** How many frames of the simulation to hold */
-  val frameMemoryLength = 60
-
-  /** How many boids to start with in the simulation */
-  val numBoids = 150
-
-  /** When the wind is blowing, how strongly it blows */
-  val windStrength = 0.03
-
-  /** The wind -- an optional acceleration vector */
-  var wind:Option[Vec2] = None
-
-  /**
-    * Sets a wind blowing at windStrength, at this angle.
-    * Note that a northerly wind blows **from** the north, so we multiply the vector by -1.
-    */
-  def setWindDirection(theta:Double):Unit = {
-    ???
-  }
-
-  /** A container that can hold a boid to add on the next frame */
-  var insertBoid:Option[Boid] = None
-
-  /**
-    * A function that will run for the next frame only over each boid in the system,
-    * producing an acceleration vector to add to a Boid
-    */
-  var oneTimeFunction:Option[Boid => Vec2] = None
-
-  /**
-    * Resets the events that should occur one time only
-    */
-  def resetOneTimeEvents():Unit = {
-    ???
-  }
-
-  /**
-    * A queue containing the last `frameMemory frames`.
-    * We're using an immutable queue, but we've put it in a var, given that this controller is mutable.
-    */
-  var frameMemory = FrameMemory(SimulationFrame.explosionOfBoids(numBoids), frameMemoryLength)
-
-  /** The current frame */
-  def current:SimulationFrame = ???
-
-  /** Called by a click to the canvas, to say that in the next frame, a boid should be inserted */
-  def pushBoid(b:Boid):Unit = {
-    insertBoid = Some(b)
-  }
-
-  /** Called by the Action Replay button to jump back in the memory buffer */
-  def resetQueue():Seq[Boid] = {
-    ???
-  }
-
-  /** Progress to the next frame in the simulation */
-  def update():Unit = {
-    ???
-  }
-
-  /** Force the simulation to use this as the next frame */
-  def pushFrame(frame:SimulationFrame):Unit = {
-    ???
-  }
-
-
-}
diff --git a/src/main/scala/cosc250/boids/SimulationFrame.scala b/src/main/scala/cosc250/boids/SimulationFrame.scala
deleted file mode 100644
index 23588e4..0000000
--- a/src/main/scala/cosc250/boids/SimulationFrame.scala
+++ /dev/null
@@ -1,57 +0,0 @@
-package cosc250.boids
-
-/**
-  * Represents the state in our simulation.
-  *
-  * A simulation frame contains an immutable sequence of Boids. It has methods for reporting various measures about
-  * the boids in the frame. It has a method for producing a new simulation frame
-  */
-case class SimulationFrame(boids:Seq[Boid]) {
-
-  /** The current average direction of the flock. Add up all the boids' velocity vectors, and take the theta. */
-  def flockDir:Double =
-    println("Warning, you haven't implemented flockDir!")
-    0d
-
-  /** The current average speed of the flock. Take the mean of all the boids' velocity magnitudes. */
-  def flockSpeed:Double =
-    println("Warning, you haven't implemented flockSpeed!")
-    0d
-
-  /**
-    * The variance of the flock's positions, ignoring the fact we wrap around the screen.
-    * To get this one:
-    *   * Calculate the centroid of the flock (Add all the position vectors, and divide by the number of boids)
-    *   * Calculate the square of the distance of each boid from this centroid, and sum them.
-    *      i.e., sum Math.pow((b.position - centroid).magnitude, 2)
-    *   * Divide this by the number of boids.
-    *
-    * We'll probably eyeball the code for this one, given we're going to find it harder to eyeball whether the number
-    * on the screen looks right!
-    */
-  def flockSep:Double =
-    println("Warning, you haven't implemented flockSep!")
-    0d
-
-  /** This function should calculate the next set of boids assuming there is no wind & no one-time functions applied */
-  def nextBoids:Seq[Boid] =
-    ???
-
-  /**
-    *
-    * @param wind - a force applied to every boid. We've called it "wind" but in practice, it'll steer the flock.
-    * @param oneTimeFunction - a function to apply to every boid (e.g. startle).
-    * @return
-    */
-  def nextFrame(wind:Option[Vec2] = None, oneTimeFunction:Option[Boid => Vec2] = None):SimulationFrame =
-    ???
-
-}
-
-object SimulationFrame {
-
-  /** Generates boids in the centre of the simulation, moving at v=1 in a random direction */
-  def explosionOfBoids(i:Int):SimulationFrame =
-    ???
-  
-}
\ No newline at end of file
diff --git a/src/main/scala/cosc250/boids/Vec2.scala b/src/main/scala/cosc250/boids/Vec2.scala
deleted file mode 100644
index d3041ab..0000000
--- a/src/main/scala/cosc250/boids/Vec2.scala
+++ /dev/null
@@ -1,117 +0,0 @@
-package cosc250.boids
-
-import scala.util.Random
-
-/**
-  * A Vec2 contains an x and a y value
-  *
-  * This could represent an (x,y) position, or an (vx, vy) velocity,
-  * or an (ax, ay) acceleration
-  *
-  * Vector addition and subtraction are implemented for you.
-  * As is multiplication by a scalar (by a number)
-  *
-  * Also implemented is converting to and from "r, theta" notation where
-  * instead of an x and y value, you have a magnitude and an angle.
-  *
-  * You might find reading the test spec useful for examples of how to call these
-  * functions
-  *
-  * @param x
-  * @param y
-  */
-case class Vec2(x:Double, y:Double) {
-
-  /** Vector addition -- adds this Vec2 to another */
-  def +(v:Vec2) = Vec2(x + v.x, y + v.y)
-
-  /** Vector subtraction -- subtracts the other vector from this one */
-  def -(v:Vec2) = Vec2(x - v.x, y - v.y)
-
-  /**
-    * Returns the angle this vector makes from the origin.
-    * Imagine this vector is an arrow on graph-paper from (0,0) to (x,y).
-    * Theta is the angle from the x axis.
-    */
-  def theta:Double = Math.atan2(y, x)
-
-  /**
-    * Returns the magnitude of this vector
-    * Imagine this vector is an arrow on graph-paper from (0,0) to (x,y).
-    * magnitude is the length of the arrow
-    * @return
-    */
-  def magnitude:Double = Math.sqrt(Math.pow(x,2) + Math.pow(y,2))
-
-  /**
-    * Multiplication by a scalar. Multiplies the x and y values by d.
-    * eg, Vec2(7,9) * 2 == Vec2(14, 18)
-    */
-  def *(d:Double) = Vec2(x * d, y * d)
-
-
-  /**
-    * Division by a scalar. Divides the x and y values by d.
-    * eg, Vec2(14, 18) / 2 == Vec2(7, 9)
-    */
-  def /(d:Double):Vec2 = *(1/d)
-
-  /**
-    * Returns a vector that has the same angle (theta) but a magnitude (arrow length)
-    * of 1
-    * @return
-    */
-  def normalised:Vec2 = Vec2.fromRTheta(1, theta)
-
-  /**
-    * If the magnitude is greater than mag, returns a vector that has the same angle
-    * (theta) but a magnitude (arrow length) of mag.
-    *
-    * Otherwise, if this vector is shorter than mag, just returns this vector.
-    * @param mag
-    * @return
-    */
-  def limit(mag:Double):Vec2 = {
-    if (magnitude > mag) {
-      Vec2.fromRTheta(mag, theta)
-    } else this
-  }
-
-}
-
-/**
-  * Companion object for Vec2. Contains functions for creating Vec2s.
-  */
-object Vec2 {
-
-  /**
-    * Takes an angle (theta) and a length (r), and returns a Vec2 representing that
-    * arrow. Note that theta is measured in radians (there are 2 * PI radians in a
-    * circle rather than 360 degrees in a circle).
-    *
-    * @param r
-    * @param theta
-    * @return
-    */
-  def fromRTheta(r:Double, theta:Double):Vec2 = {
-    Vec2(r * Math.cos(theta), r * Math.sin(theta))
-  }
-
-  /** A vector of magnitude d in a random direction */
-  def randomDir(d:Double):Vec2 = {
-    val theta = Random.nextDouble() * Math.PI * 2
-    Vec2.fromRTheta(d, theta)
-  }
-
-  val E:Double = 0
-  val SE:Double = Math.PI / 4
-  val S:Double = Math.PI / 2
-  val SW:Double = 3 * Math.PI / 4
-  val W:Double = Math.PI
-  val NW:Double = 5 * Math.PI / 4
-  val N:Double = 3 * Math.PI / 2
-  val NE:Double = 7 * Math.PI / 4
-
-
-}
-
diff --git a/src/main/scala/cosc250/reversi/App.scala b/src/main/scala/cosc250/reversi/App.scala
new file mode 100644
index 0000000..470630c
--- /dev/null
+++ b/src/main/scala/cosc250/reversi/App.scala
@@ -0,0 +1,149 @@
+package cosc250.reversi
+
+import scalafx.Includes._
+import scalafx.application.JFXApp3
+import scalafx.scene.{Scene, Group}
+import scalafx.scene.paint.Color
+import scalafx.scene.shape.*
+import scalafx.scene.layout.*
+import scalafx.scene.control.*
+import scalafx.geometry.*
+import scalafx.collections.*
+import scalafx.animation.*
+import scalafx.beans.property.*
+
+object App extends JFXApp3 {
+
+    override def start() = {
+        stage = new JFXApp3.PrimaryStage {
+            title.value = "Reversi"
+            width = 480
+            height = 600
+            scene = new Scene {
+                content = new UI().subscene                    
+            }
+        }
+    }
+
+}
+
+class UI() {
+
+    // A constant for how bix the squares are
+    val squareSize = 40
+
+    /** Represents a square on the board. */
+    class Square(col:Int, row:Int) {
+
+        private val square = new Rectangle {
+            width = squareSize
+            height = squareSize
+            fill = if (col + row) % 2 == 0 then Color.hsb(114, 0.85, 0.33) else Color.hsb(114, 0.65, 0.43)
+        }
+
+        val ui = new Group {
+            translateX = col * squareSize
+            translateY = row * squareSize
+            children = Seq(
+                square
+            )
+        }
+
+        def clear():Unit = {
+            ui.children = Seq(square)
+        }
+
+        def place(p:Player) = {
+            ui.children = Seq(
+                square,
+                new Circle {
+                    centerX = squareSize / 2
+                    centerY = squareSize / 2
+                    radius = squareSize / 3
+                    fill = if p == Player.White then Color.White else Color.Black
+                }
+            )
+        }
+    }
+
+    private val squares = for y <- 0 until boardSize yield
+        for x <- 0 until boardSize yield 
+            new Square(x, y)
+
+
+    var game = newGame
+
+    private def showGameState(g:GameState):Unit = {
+        for 
+            y <- 0 until boardSize
+            x <- 0 until boardSize
+        do
+            if g.board.contains((x, y)) then
+                squares(y)(x).place(g.board((x, y)))
+            else 
+                squares(y)(x).clear()
+    }
+
+    private def showGame(gs:Seq[GameState]):Unit = {
+        showGameState(gs.last)
+        history.items = ObservableBuffer((for 
+            (g, i) <- gs.zipWithIndex 
+            (p, loc) <- g.lastMove
+        yield
+            (i, loc, p)
+        )*)
+    }
+
+
+    val history = new ListView[(Int, Player, Location)] {
+        orientation = Orientation.Vertical
+        cellFactory = (cell, data) => {
+            val (i, p, (x, y)) = data
+            val col = "abcdefgh"(x)
+            cell.text = s"$i. $col${y+1}"
+            cell.textFill = if p == Player.Black then Color.Black else Color.White
+            cell.onMouseClicked = { _ =>
+                lastTime = System.nanoTime()
+                game = rewindTo(game, i)
+                showGame(game)                
+            }
+
+            cell.style = 
+                if i % 2 == 0 then "-fx-background-color: #1D7A12;"
+                else  "-fx-background-color: #1A6E10;"
+        }
+    }
+
+    private def step():Unit = {
+        println(s"Lookahead ${lookAhead.value.value}")
+        game = play(game, lookAhead.value.value)
+        showGame(game)
+    }
+
+    private var lastTime = 0L
+    private val playing = BooleanProperty(false)
+
+    private val lookAhead = new Spinner[Int](0, 5, 0)
+
+    AnimationTimer({ now =>
+        if playing.value && (now > lastTime + 1e9) then 
+            lastTime = now
+            step()
+    }).start()
+
+    private val startStop = new Button {
+        text <== (for p <- playing yield if p then "Stop" else "Start")
+        onAction = { _ => playing.value = !playing.value }
+    }
+
+    val subscene = new HBox(
+        new VBox(
+            new Group(squares.flatten.map(_.ui).toSeq*),
+            new HBox(5, startStop, Label("Lookahead"), lookAhead)
+        ),
+        history
+    )
+
+
+
+}
\ No newline at end of file
diff --git a/src/main/scala/cosc250/reversi/Reversi.scala b/src/main/scala/cosc250/reversi/Reversi.scala
new file mode 100644
index 0000000..945b201
--- /dev/null
+++ b/src/main/scala/cosc250/reversi/Reversi.scala
@@ -0,0 +1,63 @@
+package cosc250.reversi
+
+import scala.collection.immutable.Queue
+
+enum Player:
+    case Black
+    case White
+
+/** The board size is always 8 by 8 */
+val boardSize = 8
+
+/** A location on the board. Zero-indexed */
+type Location = (Int, Int)
+
+/**
+  * The state of the board
+  * 
+  * @param lastMove - the location of the last move
+  * @param board - maps the locations of pieces on the board (note that if a piece has not been played in a square, it won't be in the map)
+  * @param turn - whose turn it is next
+  */
+case class GameState(lastMove:Option[(Location, Player)], board:Map[Location, Player], turn:Player) {
+
+    /** The number of black pieces */
+    def blackPieces:Int = ???
+
+    /** The number of white pieces */
+    def whitePieces:Int = ???
+
+    /** True if neither player can play a move */
+    def gameOver:Boolean = ???
+
+    /** Whether a particular move is valid */
+    def isValidMove(location:Location):Boolean = 
+        ???
+
+    /** Performs a move */
+    def move(location:Location):GameState = 
+        ???
+        
+
+    // Other methods you write
+
+}
+
+object GameState {
+    def newGame = GameState(None, Map.empty, Player.Black)
+}
+
+/** A game is a sequence of game-states (so it remembers past moves). The most recent move is at the end. */
+type Game = Seq[GameState]
+
+/** Creates a new game, containing just the start game state */
+def newGame:Seq[GameState] = Seq(GameState.newGame)
+
+/** Called by the UI on each animation tick to make your AI play the game */
+def play(state:Seq[GameState], lookAhead:Int):Seq[GameState] = 
+    ???
+    
+/** Called by the UI when the user clicks back in the game histry */
+def rewindTo(state:Seq[GameState], move:Int):Seq[GameState] = 
+    ???
+
diff --git a/src/test/scala/cosc250/boids/BoidSuite.scala b/src/test/scala/cosc250/boids/BoidSuite.scala
deleted file mode 100644
index b84f339..0000000
--- a/src/test/scala/cosc250/boids/BoidSuite.scala
+++ /dev/null
@@ -1,29 +0,0 @@
-package cosc250.boids
-
-/**
-  * A place for you to write some boid tests.
-  *
-  * Boids are an immutable class containing functions. That makes them relatively straightforward to test --
-  * except that the values within them are doubles, which are hard to compare exactly. Instead, test if they
-  * are close (i.e. within a certain amount +/- what you're looking for).
-  */
-class BoidSuite extends munit.FunSuite {
-
-  // A place for you to write tests. Some suggested tests to start with have been sketched below
-
-  // Let's start with the extension methods closeTo, centroid and averageVelocity on Seq[Boid]...
-
-  test("Seq[Boid] should be able to filter just those close to a certain point") {
-    ???
-  }
-
-  test("Seq[Boid] should be able to calculate its centroid") {
-    ???
-  }
-
-  test("Seq[Boid] should be able to calculate its average velocity") {
-    ???
-  }
-
-
-}
diff --git a/src/test/scala/cosc250/boids/SimulationFrameSuite.scala b/src/test/scala/cosc250/boids/SimulationFrameSuite.scala
deleted file mode 100644
index 53f1f42..0000000
--- a/src/test/scala/cosc250/boids/SimulationFrameSuite.scala
+++ /dev/null
@@ -1,12 +0,0 @@
-package cosc250.boids
-
-/**
-  * SimulationFrames are immutable data classes with functions to methods to produce new immutable values.
-  * They are eminently testable - although you will probably want to use a small number of boids (e.g. 2 or 3) in
-  * your tests.
-  */
-class SimulationFrameSuite extends munit.FunSuite {
-
-  // A place for you to write tests
-
-}
diff --git a/src/test/scala/cosc250/boids/Vec2Suite.scala b/src/test/scala/cosc250/boids/Vec2Suite.scala
deleted file mode 100644
index 3c51a2b..0000000
--- a/src/test/scala/cosc250/boids/Vec2Suite.scala
+++ /dev/null
@@ -1,37 +0,0 @@
-package cosc250.boids
-
-/**
-  * This code is provided.
-  */
-class Vec2Suite extends munit.FunSuite {
-
-  test("Vec2 should be able to add vectors") {
-    assertEquals(Vec2(1, 2) + Vec2(3, 4), Vec2(4, 6))
-  }
-
-  test("it should be able to subtract vectors") {
-    assertEquals(Vec2(8, 9) - Vec2(3, 4), Vec2(5, 5))
-  }
-
-  test("it should be able to multiply a vector by a number") {
-    assertEquals(Vec2(8, 9) * 4, Vec2(32, 36))
-  }
-
-  test("it should be able to divide a vector by a number") {
-    assertEquals(Vec2(8, 6) / 2, Vec2(4, 3))
-  }
-
-  test("it should limit the size of a vector") {
-    assertEquals(Vec2(10, 0).limit(5), Vec2(5, 0))
-  }
-
-  test("it should calculcate vectors from a direction and an angle in radians") {
-    val Vec2(x, y) = Vec2.fromRTheta(4, Math.PI)
-
-    // There could be a rounding error -- these are doubles, so floor them as
-    // we know where Math.PI should go
-    assertEquals(Vec2(x.floor, y.floor), Vec2(-4, 0))
-  }
-
-
-}
diff --git a/src/test/scala/cosc250/reversi/ReversiSuite.scala b/src/test/scala/cosc250/reversi/ReversiSuite.scala
new file mode 100644
index 0000000..1216606
--- /dev/null
+++ b/src/test/scala/cosc250/reversi/ReversiSuite.scala
@@ -0,0 +1,31 @@
+package cosc250.reversi
+
+/**
+  * A place for you to write some boid tests.
+  *
+  * Boids are an immutable class containing functions. That makes them relatively straightforward to test --
+  * except that the values within them are doubles, which are hard to compare exactly. Instead, test if they
+  * are close (i.e. within a certain amount +/- what you're looking for).
+  */
+class ReversiSuite extends munit.FunSuite {
+
+  // A place for you to write tests. Some suggested tests to start with have been sketched below
+
+  test("Counts pieces") {
+    assertEquals(2, GameState(None, Map((3, 3) -> Player.Black, (3, 4) -> Player.Black), Player.White).blackPieces)
+    assertEquals(2, GameState(None, Map((3, 3) -> Player.White, (3, 4) -> Player.White), Player.White).whitePieces)
+
+    // add some more!
+  }
+
+  test("Should be able to detect if a move is valid") {
+    ???
+  }
+
+  test("Should be able to count the score for one player") {
+    ???
+  }
+
+  // You'll need to write some additional tests
+
+}
-- 
GitLab